Compare commits

..

1 Commits

Author SHA1 Message Date
Alessio Gravili
d27d09aa9e perf: page loading states 2025-02-11 17:07:22 -07:00
790 changed files with 6106 additions and 55206 deletions

6
.github/CODEOWNERS vendored
View File

@@ -11,12 +11,12 @@
/packages/live-preview*/src/ @jacobsfletch
/packages/plugin-stripe/src/ @jacobsfletch
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
/packages/richtext-*/src/ @AlessioGr
/packages/richtext-*/src/ @AlessioGr @GermanJablo
/packages/next/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
/packages/ui/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
### Templates
@@ -25,7 +25,7 @@
### Build Files
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
### Root

View File

@@ -283,7 +283,6 @@ jobs:
- fields-relationship
- fields__collections__Array
- fields__collections__Blocks
- fields__collections__Blocks#config.blockreferences.ts
- fields__collections__Checkbox
- fields__collections__Collapsible
- fields__collections__ConditionalLogic
@@ -294,7 +293,6 @@ jobs:
- fields__collections__JSON
- fields__collections__Lexical__e2e__main
- fields__collections__Lexical__e2e__blocks
- fields__collections__Lexical__e2e__blocks#config.blockreferences.ts
- fields__collections__Number
- fields__collections__Point
- fields__collections__Radio

View File

@@ -19,7 +19,6 @@
// Load .git-blame-ignore-revs file
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
"jestrunner.changeDirectoryToWorkspaceRoot": false,
"jestrunner.debugOptions": {
"runtimeArgs": ["--no-deprecation"]
},

550
docs/admin/components.mdx Normal file
View File

@@ -0,0 +1,550 @@
---
title: Swap in your own React components
label: Custom Components
order: 20
desc: Fully customize your Admin Panel by swapping in your own React components. Add fields, remove views, update routes and change functions to sculpt your perfect Dashboard.
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The Payload [Admin Panel](./overview) is designed to be as minimal and straightforward as possible to allow for easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your [Payload Config](../configuration/overview).
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default. This enables the use of the [Local API](../local-api/overview) directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control.
<Banner type="success">
**Note:**
Client Components continue to be fully supported. To use Client Components in your app, simply include the `use client` directive. Payload will automatically detect and remove all default, [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component. [More details](#client-components).
</Banner>
There are four main types of Custom Components in Payload:
- [Root Components](#root-components)
- [Collection Components](../configuration/collections/#custom-components)
- [Global Components](../configuration/globals#custom-components)
- [Field Components](../fields/overview#custom-components)
To swap in your own Custom Component, first consult the list of available components, determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-components) accordingly.
## Defining Custom Components
As Payload compiles the Admin Panel, it checks your config for Custom Components. When detected, Payload either replaces its own default component with yours, or if none exists by default, renders yours outright. While are many places where Custom Components are supported in Payload, each is defined in the same way using [Component Paths](#component-paths).
To add a Custom Component, point to its file path in your Payload Config:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
Button: '/src/components/Logout#MyComponent' // highlight-line
}
}
},
})
```
<Banner type="success">
**Note:**
All Custom Components can be either Server Components or Client Components, depending on the presence of the `use client` directive at the top of the file.
</Banner>
### Component Paths
In order to ensure the Payload Config is fully Node.js compatible and as lightweight as possible, components are not directly imported into your config. Instead, they are identified by their file path for the Admin Panel to resolve on its own.
Component Paths, by default, are relative to your project's base directory. This is either your current working directory, or the directory specified in `config.admin.baseDir`. To simplify Component Paths, you can also configure the base directory using the `admin.importMap.baseDir` property.
Components using named exports are identified either by appending `#` followed by the export name, or using the `exportName` property. If the component is the default export, this can be omitted.
```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: {
importMap: {
baseDir: path.resolve(dirname, 'src'), // highlight-line
},
components: {
logout: {
Button: '/components/Logout#MyComponent' // highlight-line
}
}
},
})
```
In this example, we set the base directory to the `src` directory, and omit the `/src/` part of our component path string.
### Config Options
While Custom Components are usually defined as a string, you can also pass in an object with additional options:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
// highlight-start
Button: {
path: '/src/components/Logout',
exportName: 'MyComponent',
}
// highlight-end
}
}
},
})
```
The following options are available:
| Property | Description |
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
| **`clientProps`** | Props to be passed to the Custom Components if it's a Client Component. [More details](#custom-props). |
| **`exportName`** | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
| **`path`** | File path to the Custom Component. Named exports can be appended to the end of the path, separated by a `#`. |
| **`serverProps`** | Props to be passed to the Custom Component if it's a Server Component. [More details](#custom-props). |
For more details on how to build Custom Components, see [Building Custom Components](#building-custom-components).
### Import Map
In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
The Import Map is automatically regenerated at startup and whenever Hot Module Replacement (HMR) runs, or you can run `payload generate:importmap` to manually regenerate it.
#### Custom Imports
If needed, custom items can be appended onto the Import Map. This is mostly only relevant for plugin authors who need to add a custom import that is not referenced in a known location.
To add a custom import to the Import Map, use the `admin.dependencies` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// ...
dependencies: {
myTestComponent: { // myTestComponent is the key - can be anything
path: '/components/TestComponent.js#TestComponent',
type: 'component',
clientProps: {
test: 'hello',
},
},
},
}
}
```
## Building Custom Components
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default. This enables the use of the [Local API](../local-api/overview) directly on the front-end, among other things.
### Default Props
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](../local-api/overview) class and the [`i18n`](../configuration/i18n) object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself.
Here is an example:
```tsx
import React from 'react'
const MyServerComponent = async ({
payload // highlight-line
}) => {
const page = await payload.findByID({
collection: 'pages',
id: '123',
})
return (
<p>{page.title}</p>
)
}
```
Each Custom Component receives the following props by default:
| Prop | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `payload` | The [Payload](../local-api/overview) class. |
| `i18n` | The [i18n](../configuration/i18n) object. |
<Banner type="warning">
**Reminder:**
All Custom Components also receive various other props that are specific component being rendered. See [Root Components](#root-components), [Collection Components](../configuration/collections#custom-components), [Global Components](../configuration/globals#custom-components), or [Field Components](../fields/overview#custom-components) for a complete list of all default props per component.
</Banner>
### Custom Props
To pass in custom props from the config, you can use either the `clientProps` or `serverProps` properties depending on whether your prop is [serializable](https://react.dev/reference/rsc/use-client#serializable-types), and whether your component is a Server or Client Component.
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: { // highlight-line
components: {
logout: {
Button: {
path: '/src/components/Logout#MyComponent',
clientProps: {
myCustomProp: 'Hello, World!' // highlight-line
},
}
}
}
},
})
```
```tsx
'use client'
import React from 'react'
export const MyComponent = ({ myCustomProp }: { myCustomProp: string }) => {
return (
<button>{myCustomProp}</button>
)
}
```
### Client Components
When [Building Custom Components](#building-custom-components), it's still possible to use client-side code such as `useState` or the `window` object. To do this, simply add the `use client` directive at the top of your file. Payload will automatically detect and remove all default, [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component.
```tsx
'use client' // highlight-line
import React, { useState } from 'react'
export const MyClientComponent: React.FC = () => {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
)
}
```
<Banner type="warning">
**Reminder:**
Client Components cannot be passed [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types). If you are rendering your Client Component _from within_ a Server Component, ensure that its props are serializable.
</Banner>
### Accessing the Payload Config
From any Server Component, the [Payload Config](../configuration/overview) can be accessed directly from the `payload` prop:
```tsx
import React from 'react'
export default async function MyServerComponent({
payload: {
config // highlight-line
}
}) {
return (
<Link href={config.serverURL}>
Go Home
</Link>
)
}
```
But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions, React components, etc. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](./hooks#useconfig) hook:
```tsx
'use client'
import React from 'react'
import { useConfig } from '@payloadcms/ui'
export const MyClientComponent: React.FC = () => {
const { config: { serverURL } } = useConfig() // highlight-line
return (
<Link href={serverURL}>
Go Home
</Link>
)
}
```
<Banner type="success">
See [Using Hooks](#using-hooks) for more details.
</Banner>
All [Field Components](../fields/overview#custom-components) automatically receive their respective Field Config through props.
```tsx
import React from 'react'
import type { TextFieldServerComponent } from 'payload'
export const MyClientFieldComponent: TextFieldServerComponent = ({ field: { name } }) => {
return (
<p>
{`This field's name is ${name}`}
</p>
)
}
```
### Getting the Current Language
All Custom Components can support multiple languages to be consistent with Payload's [Internationalization](../configuration/i18n). To do this, first add your translation resources to the [I18n Config](../configuration/i18n).
From any Server Component, you can translate resources using the `getTranslation` function from `@payloadcms/translations`. All Server Components automatically receive the `i18n` object as a prop by default.
```tsx
import React from 'react'
import { getTranslation } from '@payloadcms/translations'
export default async function MyServerComponent({ i18n }) {
const translatedTitle = getTranslation(myTranslation, i18n) // highlight-line
return (
<p>{translatedTitle}</p>
)
}
```
The best way to do this within a Client Component is to import the `useTranslation` hook from `@payloadcms/ui`:
```tsx
'use client'
import React from 'react'
import { useTranslation } from '@payloadcms/ui'
export const MyClientComponent: React.FC = () => {
const { t, i18n } = useTranslation() // highlight-line
return (
<ul>
<li>{t('namespace1:key', { variable: 'value' })}</li>
<li>{t('namespace2:key', { variable: 'value' })}</li>
<li>{i18n.language}</li>
</ul>
)
}
```
<Banner type="success">
See the [Hooks](./hooks) documentation for a full list of available hooks.
</Banner>
### Getting the Current Locale
All [Custom Views](./views) can support multiple locales to be consistent with Payload's [Localization](../configuration/localization). They automatically receive the `locale` object as a prop by default. This can be used to scope API requests, etc.:
```tsx
import React from 'react'
export default async function MyServerComponent({ payload, locale }) {
const localizedPage = await payload.findByID({
collection: 'pages',
id: '123',
locale,
})
return (
<p>{localizedPage.title}</p>
)
}
```
The best way to do this within a Client Component is to import the `useLocale` hook from `@payloadcms/ui`:
```tsx
'use client'
import React from 'react'
import { useLocale } from '@payloadcms/ui'
const Greeting: React.FC = () => {
const locale = useLocale() // highlight-line
const trans = {
en: 'Hello',
es: 'Hola',
}
return (
<span>{trans[locale.code]}</span>
)
}
```
<Banner type="success">
See the [Hooks](./hooks) documentation for a full list of available hooks.
</Banner>
### 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. To do this, you can one of the many hooks available depending on your needs.
```tsx
'use client'
import React from 'react'
import { useDocumentInfo } from '@payloadcms/ui'
export const MyClientComponent: React.FC = () => {
const { slug } = useDocumentInfo() // highlight-line
return (
<p>{`Entity slug: ${slug}`}</p>
)
}
```
<Banner type="success">
See the [Hooks](./hooks) documentation for a full list of available hooks.
</Banner>
### Adding Styles
Payload has a robust [CSS Library](./customizing-css) that you can use to style your Custom Components similarly to Payload's built-in styling. This will ensure that your Custom Components match the existing design system, and so that they automatically adapt to any theme changes that might occur.
To apply custom styles, simply import your own `.css` or `.scss` file into your Custom Component:
```tsx
import './index.scss'
export const MyComponent: React.FC = () => {
return (
<div className="my-component">
My Custom Component
</div>
)
}
```
Then to colorize your Custom Component's background, for example, you can use the following CSS:
```scss
.my-component {
background-color: var(--theme-elevation-500);
}
```
Payload also exports its [SCSS](https://sass-lang.com) library for reuse which includes mixins, etc. To use this, simply import it as follows into your `.scss` file:
```scss
@import '~@payloadcms/ui/scss';
.my-component {
@include mid-break {
background-color: var(--theme-elevation-900);
}
}
```
<Banner type="success">
**Note:**
You can also drill into Payload's own component styles, or easily apply global, app-wide CSS. More on that [here](./customizing-css).
</Banner>
## Root Components
Root Components are those that affect the [Admin Panel](./overview) generally, such as the logo or the main nav.
To override Root Components, use the `admin.components` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
// ...
},
// highlight-end
},
})
```
_For details on how to build Custom Components, see [Building Custom Components](#building-custom-components)._
The following options are available:
| Path | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`Nav`** | Contains the sidebar / mobile menu in its entirety. |
| **`beforeNavLinks`** | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. |
| **`afterNavLinks`** | An array of Custom Components to inject into the built-in Nav, _after_ the links. |
| **`beforeDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
| **`afterDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _after_ the default dashboard contents. |
| **`beforeLogin`** | An array of Custom Components to inject into the built-in Login, _before_ the default login form. |
| **`afterLogin`** | An array of Custom Components to inject into the built-in Login, _after_ the default login form. |
| **`logout.Button`** | The button displayed in the sidebar that logs the user out. |
| **`graphics.Icon`** | The simplified logo used in contexts like the the `Nav` component. |
| **`graphics.Logo`** | The full logo used in contexts like the `Login` view. |
| **`providers`** | Custom [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) providers that will wrap the entire Admin Panel. [More details](#custom-providers). |
| **`actions`** | An array of Custom Components to be rendered _within_ the header of the Admin Panel, providing additional interactivity and functionality. |
| **`header`** | An array of Custom Components to be injected above the Payload header. |
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
<Banner type="success">
**Note:**
You can also use set [Collection Components](../configuration/collections#custom-components) and [Global Components](../configuration/globals#custom-components) in their respective configs.
</Banner>
### Custom Providers
As you add more and more Custom Components to your [Admin Panel](./overview), you may find it helpful to add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context)(s). Payload allows you to inject your own context providers in your app so you can export your own custom hooks, etc.
To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
components: {
providers: ['/path/to/MyProvider'], // highlight-line
},
},
})
```
Then build your Custom Provider as follows:
```tsx
'use client'
import React, { createContext, useContext } from 'react'
const MyCustomContext = React.createContext(myCustomValue)
export const MyProvider: React.FC = ({ children }) => {
return (
<MyCustomContext.Provider value={myCustomValue}>
{children}
</MyCustomContext.Provider>
)
}
export const useMyCustomContext = () => useContext(MyCustomContext)
```
<Banner type="warning">
**Reminder:** React Context exists only within Client Components. This means they must include the `use client` directive at the top of their files and cannot contain server-only code. To use a Server Component here, simply _wrap_ your Client Component with it.
</Banner>

View File

@@ -1,7 +1,7 @@
---
title: Customizing CSS & SCSS
label: Customizing CSS
order: 50
order: 80
desc: Customize the Payload Admin Panel further by adding your own CSS or SCSS style sheet to the configuration, powerful theme and design options are waiting for you.
keywords: admin, css, scss, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
@@ -30,7 +30,7 @@ Here is an example of how you might target the Dashboard View and change the bac
<Banner type="warning">
**Note:**
If you are building [Custom Components](../custom-components/overview), it is best to import your own stylesheets directly into your components, rather than using the global stylesheet. You can continue to use the [CSS library](#css-library) as needed.
If you are building [Custom Components](./components), it is best to import your own stylesheets directly into your components, rather than using the global stylesheet. You can continue to use the [CSS library](#css-library) as needed.
</Banner>
### Specificity rules

View File

@@ -1,16 +1,16 @@
---
title: React Hooks
label: React Hooks
order: 50
order: 40
desc: Make use of all of the powerful React hooks that Payload provides.
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Payload provides a variety of powerful [React Hooks](https://react.dev/reference/react-dom/hooks) that can be used within your own [Custom Components](../custom-components/overview), such as [Custom Fields](../fields/overview#custom-components). With them, you can interface with Payload itself to build just about any type of complex customization you can think of.
Payload provides a variety of powerful [React Hooks](https://react.dev/reference/react-dom/hooks) that can be used within your own [Custom Components](./components), such as [Custom Fields](../fields/overview#custom-components). With them, you can interface with Payload itself to build just about any type of complex customization you can think of.
<Banner type="warning">
**Reminder:**
All Custom Components are [React Server Components](https://react.dev/reference/rsc/server-components) by default. Hooks, on the other hand, are only available in client-side environments. To use hooks, [ensure your component is a client component](../custom-components/overview#client-components).
All Custom Components are [React Server Components](https://react.dev/reference/rsc/server-components) by default. Hooks, on the other hand, are only available in client-side environments. To use hooks, [ensure your component is a client component](./components#client-components).
</Banner>
## useField
@@ -875,7 +875,7 @@ const Greeting: React.FC = () => {
## useConfig
Used to retrieve the Payload [Client Config](../custom-components/overview#accessing-the-payload-config).
Used to retrieve the Payload [Client Config](./components#accessing-the-payload-config).
```tsx
'use client'
@@ -1113,40 +1113,5 @@ setParams({ depth: 2 })
This is useful for scenarios where you need to trigger another fetch regardless of the `url` argument changing.
## useRouteTransition
Route transitions are useful in showing immediate visual feedback to the user when navigating between pages. This is especially useful on slow networks when navigating to data heavy or process intensive pages.
By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions dy default.
```tsx
import { Link } from '@payloadcms/ui'
const MyComponent = () => {
return (
<Link href="/somewhere">
Go Somewhere
</Link>
)
}
```
You can also trigger route transitions programmatically, such as when using `router.push` from `next/router`. To do this, wrap your function calls with the `startRouteTransition` method provided by the `useRouteTransition` hook.
```ts
'use client'
import React, { useCallback } from 'react'
import { useTransition } from '@payloadcms/ui'
import { useRouter } from 'next/navigation'
const MyComponent: React.FC = () => {
const router = useRouter()
const { startRouteTransition } = useRouteTransition()
const redirectSomewhere = useCallback(() => {
startRouteTransition(() => router.push('/somewhere'))
}, [startRouteTransition, router])
// ...
}
```

View File

@@ -1,7 +1,7 @@
---
title: Document Locking
label: Document Locking
order: 40
order: 90
desc: Ensure your documents are locked during editing to prevent concurrent changes from multiple users and maintain data integrity.
keywords: locking, document locking, edit locking, document, concurrency, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
---

View File

@@ -194,7 +194,7 @@ The Global Meta config has the same options as the [Root Metadata](#root-metadat
## View Metadata
View Metadata is the metadata that is applied to specific [Views](../custom-components/custom-views) within the Admin Panel. This metadata is used to customize the title and description of a specific view, overriding any metadata set at the [Root](#root-metadata), [Collection](#collection-metadata), or [Global](#global-metadata) level.
View Metadata is the metadata that is applied to specific [Views](./views) within the Admin Panel. This metadata is used to customize the title and description of a specific view, overriding any metadata set at the [Root](#root-metadata), [Collection](#collection-metadata), or [Global](#global-metadata) level.
To customize View Metadata, use the `meta` key within your View Config:

View File

@@ -8,12 +8,12 @@ keywords: admin, components, custom, customize, documentation, Content Managemen
Payload dynamically generates a beautiful, [fully type-safe](../typescript/overview) Admin Panel to manage your users and data. It is highly performant, even with 100+ fields, and is translated in over 30 languages. Within the Admin Panel you can manage content, [render your site](../live-preview/overview), [preview drafts](./preview), [diff versions](../versions/overview), and so much more.
The Admin Panel is designed to [white-label your brand](https://payloadcms.com/blog/white-label-admin-ui). You can endlessly customize and extend the Admin UI by swapping in your own [Custom Components](../custom-components/overview)—everything from simple field labels to entire views can be modified or replaced to perfectly tailor the interface for your editors.
The Admin Panel is designed to [white-label your brand](https://payloadcms.com/blog/white-label-admin-ui). You can endlessly customize and extend the Admin UI by swapping in your own [Custom Components](./components)—everything from simple field labels to entire views can be modified or replaced to perfectly tailor the interface for your editors.
The Admin Panel is written in [TypeScript](https://www.typescriptlang.org) and built with [React](https://react.dev) using the [Next.js App Router](https://nextjs.org/docs/app). It supports [React Server Components](https://react.dev/reference/rsc/server-components), enabling the use of the [Local API](/docs/local-api/overview) on the front-end. You can install Payload into any [existing Next.js app in just one line](../getting-started/installation) and [deploy it anywhere](../production/deployment).
<Banner type="success">
The Payload Admin Panel is designed to be as minimal and straightforward as possible to allow easy customization and control. [Learn more](../custom-components/overview).
The Payload Admin Panel is designed to be as minimal and straightforward as possible to allow easy customization and control. [Learn more](./components).
</Banner>
<LightDarkImage
@@ -91,7 +91,7 @@ The following options are available:
| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
| **`buildPath`** | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](./components). |
| **`custom`** | Any custom properties you wish to pass to the Admin Panel. |
| **`dateFormat`** | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
@@ -178,7 +178,7 @@ The following options are available:
<Banner type="success">
**Tip:**
You can easily add _new_ routes to the Admin Panel through [Custom Endpoints](../rest-api/overview#custom-endpoints) and [Custom Views](../custom-components/custom-views).
You can easily add _new_ routes to the Admin Panel through [Custom Endpoints](../rest-api/overview#custom-endpoints) and [Custom Views](./views).
</Banner>
#### Customizing Root-level Routes
@@ -233,7 +233,7 @@ The following options are available:
<Banner type="success">
**Note:**
You can also swap out entire _views_ out for your own, using the `admin.views` property of the Payload Config. See [Custom Views](../custom-components/custom-views) for more information.
You can also swap out entire _views_ out for your own, using the `admin.views` property of the Payload Config. See [Custom Views](./views) for more information.
</Banner>
## I18n

View File

@@ -1,7 +1,7 @@
---
title: Managing User Preferences
label: Preferences
order: 60
order: 70
desc: Store the preferences of your users as they interact with the Admin Panel.
keywords: admin, preferences, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
@@ -81,7 +81,7 @@ import { usePreferences } from '@payloadcms/ui'
const lastUsedColorsPreferenceKey = 'last-used-colors';
export function CustomComponent() {
const CustomComponent = (props) => {
const { getPreference, setPreference } = usePreferences();
// Store the last used colors in local state
@@ -154,6 +154,8 @@ export function CustomComponent() {
</Fragment>
)}
</div>
)
}
);
};
export default CustomComponent;
```

View File

@@ -1,7 +1,7 @@
---
title: Preview
label: Preview
order: 30
order: 50
desc: Enable links to your front-end to preview published or draft content.
keywords: admin, components, preview, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---

375
docs/admin/views.mdx Normal file
View File

@@ -0,0 +1,375 @@
---
title: Customizing Views
label: Customizing Views
order: 30
desc:
keywords:
---
Views are the individual pages that make up the [Admin Panel](./overview), such as the Dashboard, List, and Edit views. One of the most powerful ways to customize the Admin Panel is to create Custom Views. These are [Custom Components](./components) that can either replace built-in views or can be entirely new.
There are four types of views within the Admin Panel:
- [Root Views](#root-views)
- [Collection Views](#collection-views)
- [Global Views](#global-views)
- [Document Views](#document-views)
To swap in your own Custom View, first consult the list of available components, determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-views) accordingly.
## Root Views
Root Views are the main views of the [Admin Panel](./overview). These are views that are scoped directly under the `/admin` route, such as the Dashboard or Account views.
To swap Root Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property of your root [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
customView: {
Component: '/path/to/MyCustomView#MyCustomView', // highlight-line
path: '/my-custom-view',
}
},
},
},
})
```
Your Custom Root Views can optionally use one of the templates that Payload provides. The most common of these is the Default Template which provides the basic layout and navigation. Here is an example of what that might look like:
```tsx
import type { AdminViewProps } from 'payload'
import { DefaultTemplate } from '@payloadcms/next/templates'
import { Gutter } from '@payloadcms/ui'
import React from 'react'
export const MyCustomView: React.FC<AdminViewProps> = ({
initPageResult,
params,
searchParams,
}) => {
return (
<DefaultTemplate
i18n={initPageResult.req.i18n}
locale={initPageResult.locale}
params={params}
payload={initPageResult.req.payload}
permissions={initPageResult.permissions}
searchParams={searchParams}
user={initPageResult.req.user || undefined}
visibleEntities={initPageResult.visibleEntities}
>
<Gutter>
<h1>Custom Default Root View</h1>
<p>This view uses the Default Template.</p>
</Gutter>
</DefaultTemplate>
)
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
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). |
For more granular control, pass a configuration object instead. Payload exposes the following properties for each view:
| Property | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`Component`** * | Pass in the component path that should be rendered when a user navigates to this route. |
| **`path`** * | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
| **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
| **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
| **`sensitive`** | When true, will match if the path is case sensitive.|
| **`meta`** | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
_* An asterisk denotes that a property is required._
### Adding New Views
To add a _new_ views to the [Admin Panel](./overview), simply add your own key to the `views` object with at least a `path` and `Component` property. For example:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
// highlight-start
myCustomView: {
// highlight-end
Component: '/path/to/MyCustomView#MyCustomViewComponent',
path: '/my-custom-view',
},
},
},
},
})
```
The above example shows how to add a new [Root View](#root-views), but the pattern is the same for [Collection Views](#collection-views), [Global Views](#global-views), and [Document Views](#document-views). For help on how to build your own Custom Views, see [Building Custom Views](#building-custom-views).
<Banner type="warning">
**Note:**
Routes are cascading, so unless explicitly given the `exact` property, they will
match on URLs that simply _start_ with the route's path. This is helpful when creating catch-all
routes in your application. Alternatively, define your nested route _before_ your parent
route.
</Banner>
<Banner type="warning">
**Custom views are public**
Custom views are public by default. If your view requires a user to be logged in or to have certain access rights, you should handle that within your view component yourself.
</Banner>
## Collection Views
Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views.
To swap out Collection Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property of your [Collection Config](../configuration/collections):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
admin: {
components: {
views: {
edit: {
root: {
Component: '/path/to/MyCustomEditView', // highlight-line
}
// other options include:
// default
// versions
// version
// api
// livePreview
// [key: string]
// See "Document Views" for more details
},
list: {
Component: '/path/to/MyCustomListView',
}
},
},
},
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
<Banner type="warning">
**Note:**
The `root` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `edit.default` key instead.
</Banner>
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. |
<Banner type="success">
**Note:**
You can also add _new_ Collection Views to the config by adding a new key to the `views` object with at least a `path` and `Component` property. See [Adding New Views](#adding-new-views) for more information.
</Banner>
## Global Views
Global Views are views that are scoped under the `/globals` route, such as the Document Edit View.
To swap out Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../configuration/globals):
```ts
import type { SanitizedGlobalConfig } from 'payload'
export const MyGlobalConfig: SanitizedGlobalConfig = {
// ...
admin: {
components: {
views: {
edit: {
root: {
Component: '/path/to/MyCustomEditView', // highlight-line
}
// other options include:
// default
// versions
// version
// api
// livePreview
// [key: string]
},
},
},
},
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
<Banner type="warning">
**Note:**
The `root` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `edit.default` key instead.
</Banner>
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). |
<Banner type="success">
**Note:**
You can also add _new_ Global Views to the config by adding a new key to the `views` object with at least a `path` and `Component` property. See [Adding New Views](#adding-new-views) for more information.
</Banner>
## Document Views
Document Views are views that are scoped under the `/collections/:collectionSlug/:id` or the `/globals/:globalSlug` route, such as the Edit View or the API View. All Document Views keep their overall structure across navigation changes, such as their title and tabs, and replace only the content below.
To swap out Document Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views.Edit[key]` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionOrGlobalConfig: SanitizedCollectionConfig = {
// ...
admin: {
components: {
views: {
edit: {
api: {
Component: '/path/to/MyCustomAPIViewComponent', // highlight-line
},
},
},
},
},
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
<Banner type="warning">
**Note:**
If you need to replace the _entire_ Edit View, including _all_ nested Document Views, use the `root` key. See [Custom Collection Views](#collection-views) or [Custom Global Views](#global-views) for more information.
</Banner>
The following options are available:
| Property | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`root`** | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. |
| **`default`** | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. |
| **`versions`** | The Versions View is used to navigate the version history of a single document. It is rendered within the "Versions" tab. [More details](../versions/overview). |
| **`version`** | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](../versions/overview). |
| **`api`** | The API View is used to display the REST API JSON response for a given document. It is rendered within the "API" tab. |
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. It is rendered within the "Live Preview" tab. [More details](../live-preview/overview). |
### Document Tabs
Each Custom 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'
export const MyCollection: SanitizedCollectionConfig = {
slug: 'my-collection',
admin: {
components: {
views: {
edit: {
myCustomTab: {
Component: '/path/to/MyCustomTab',
path: '/my-custom-tab',
tab: {
Component: '/path/to/MyCustomTabComponent' // highlight-line
}
},
anotherCustomTab: {
Component: '/path/to/AnotherCustomView',
path: '/another-custom-view',
// highlight-start
tab: {
label: 'Another Custom View',
href: '/another-custom-view',
}
// highlight-end
},
},
},
},
},
}
```
<Banner type="warning">
**Note:**
This applies to _both_ Collections _and_ Globals.
</Banner>
## Building Custom Views
Custom Views are just [Custom Components](./components) rendered at the page-level. To understand how to build Custom Views, first review the [Building Custom Components](./components#building-custom-components) guide. Once you have a Custom Component ready, you can use it as a Custom View.
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
admin: {
components: {
views: {
edit: {
Component: '/path/to/MyCustomView' // highlight-line
}
},
},
},
}
```
### Default Props
Your Custom Views will be provided with the following props:
| Prop | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **`initPageResult`** | An object containing `req`, `payload`, `permissions`, etc. |
| **`clientConfig`** | The Client Config object. [More details](../admin/components#accessing-the-payload-config). |
| **`importMap`** | The import map object. |
| **`params`** | An object containing the [Dynamic Route Parameters](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes). |
| **`searchParams`** | An object containing the [Search Parameters](https://developer.mozilla.org/docs/Learn/Common_questions/What_is_a_URL#parameters). |
| **`doc`** | The document being edited. Only available in Document Views. [More details](#document-views). |
<Banner type="success">
**Reminder:**
All [Custom Server Components](./components) receive `payload` and `i18n` by default. See [Building Custom Components](./components#building-custom-components) for more details.
</Banner>
<Banner type="warning">
**Important:**
It's up to you to secure your custom views. If your view requires a user to be logged in or to
have certain access rights, you should handle that within your view component yourself.
</Banner>

View File

@@ -59,25 +59,25 @@ The following options are available:
| Option | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| **`admin`** | The configuration options for the Admin Panel. [More details](#admin-options). |
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| **`auth`** | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| **`defaultSort`** | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
| **`dbName`** | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| **`graphQL`** | Manage GraphQL-related properties for this collection. [More](#graphql) |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| **`defaultPopulate`** | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
_* An asterisk denotes that a property is required._
@@ -95,7 +95,7 @@ Fields define the schema of the Documents within a Collection. To learn more, go
## Admin Options
The behavior of Collections within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](../custom-components/overview), selecting which fields to display in the List View, and more.
The behavior of Collections within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](../admin/components), selecting which fields to display in the List View, and more.
To configure Admin Options for Collections, use the `admin` property in your Collection Config:
@@ -114,33 +114,33 @@ The following options are available:
| Option | Description |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `group` | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
| `hooks` | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
| `description` | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this Collection's List View. |
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
| `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). |
| `baseListFilter` | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
| **`group`** | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
| **`hooks`** | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
| **`description`** | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this Collection. |
| **`enableRichTextLink`** | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| **`enableRichTextRelationship`** | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| **`meta`** | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
| **`preview`** | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`components`** | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
| **`listSearchableFields`** | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
| **`pagination`** | Set pagination-specific options for this Collection. [More details](#pagination). |
| **`baseListFilter`** | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
### Custom Components
Collections can set their own [Custom Components](../custom-components/overview) which only apply to Collection-specific UI within the [Admin Panel](../admin/overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
Collections can set their own [Custom Components](../admin/components) which only apply to Collection-specific UI within the [Admin Panel](../admin/overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
To override Collection Components, use the `admin.components` property in your Collection Config:
```ts
import type { CollectionConfig } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
export const MyCollection: SanitizedCollectionConfig = {
// ...
admin: {
components: { // highlight-line
@@ -152,47 +152,23 @@ export const MyCollection: CollectionConfig = {
The following options are available:
| Option | Description |
| --------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `afterList` | An array of components to inject _after_ the built-in List View. [More details](../custom-components/list-view#afterlist). |
| `afterListTable` | An array of components to inject _after_ the built-in List View's table. [More details](../custom-components/list-view#afterlisttable). |
| `beforeList` | An array of components to inject _before_ the built-in List View. [More details](../custom-components/list-view#beforelist). |
| `beforeListTable` | An array of components to inject _before_ the built-in List View's table. [More details](../custom-components/list-view#beforelisttable). |
| `listMenuItems` | An array of components to render within a menu next to the List Controls (after the Columns and Filters options) |
| `Description` | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. [More details](../custom-components/list-view#description). |
| `edit` | Override specific components within the Edit View. [More details](#edit-view-options). |
| `views` | Override or create new views within the Admin Panel. [More details](../custom-components/custom-views). |
#### Edit View Options
```ts
import type { CollectionCOnfig } from 'payload'
export const MyCollection: CollectionCOnfig = {
// ...
admin: {
components: {
edit: { // highlight-line
// ...
},
},
},
}
```
The following options are available:
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#save-button). |
| `SaveDraftButton` | Replace the default Save Draft Button within the Edit View. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#save-draft-button). |
| `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publish-button). |
| `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#preview-button). |
| `Upload` | Replace the default Upload component within the Edit View. [Upload](../upload/overview) must be enabled. [More details](../custom-components/edit-view#upload). |
| Path | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **`beforeList`** | An array of components to inject _before_ the built-in List View |
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
| **`afterList`** | An array of components to inject _after_ the built-in List View |
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
| **`edit.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
| **`edit.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](../admin/preview) must be enabled. |
| **`edit.Upload`** | Replace the default Upload component with a Custom Component. [Upload](../upload/overview) must be enabled. |
| **`views`** | Override or create new views within the Admin Panel. [More details](../admin/views). |
<Banner type="success">
**Note:**
For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).
</Banner>
### Pagination
@@ -256,10 +232,10 @@ You can also pass an object to the collection's `graphQL` property, which allows
| Option | Description |
| ---------------------- | ----------------------------------------------------------------------------------- |
| `singularName` | Override the "singular" name that will be used in GraphQL schema generation. |
| `pluralName` | Override the "plural" name that will be used in GraphQL schema generation. |
| `disableQueries` | Disable all GraphQL queries that correspond to this collection by passing `true`. |
| `disableMutations` | Disable all GraphQL mutations that correspond to this collection by passing `true`. |
| **`singularName`** | Override the "singular" name that will be used in GraphQL schema generation. |
| **`pluralName`** | Override the "plural" name that will be used in GraphQL schema generation. |
| **`disableQueries`** | Disable all GraphQL queries that correspond to this collection by passing `true`. |
| **`disableMutations`** | Disable all GraphQL mutations that correspond to this collection by passing `true`. |
## TypeScript

View File

@@ -42,7 +42,7 @@ export default buildConfig({
For security and safety reasons, the [Admin Panel](../admin/overview) does **not** include Environment Variables in its _client-side_ bundle by default. But, Next.js provides a mechanism to expose Environment Variables to the client-side bundle when needed.
If you are building a [Custom Component](../custom-components/overview) and need to access Environment Variables from the client-side, you can do so by prefixing them with `NEXT_PUBLIC_`.
If you are building a [Custom Component](../admin/components) and need to access Environment Variables from the client-side, you can do so by prefixing them with `NEXT_PUBLIC_`.
<Banner type="warning">
**Important:**

View File

@@ -67,20 +67,20 @@ The following options are available:
| Option | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `access` | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
| `dbName` | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. |
| `description` | Text or React component to display below the Global header to give editors more information. |
| `endpoints` | Add custom routes to the REST API. [More details](../rest-api/overview#custom-endpoints). |
| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). |
| `graphQL` | Manage GraphQL-related properties related to this global. [More details](#graphql) |
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#global-hooks). |
| `label` | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. |
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Global. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#global-config). |
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
| **`admin`** | The configuration options for the Admin Panel. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`dbName`** | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. |
| **`description`** | Text or React component to display below the Global header to give editors more information. |
| **`endpoints`** | Add custom routes to the REST API. [More details](../rest-api/overview#custom-endpoints). |
| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). |
| **`graphQL`** | Manage GraphQL-related properties related to this global. [More details](#graphql) |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#global-hooks). |
| **`label`** | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. |
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#global-config). |
_* An asterisk denotes that a property is required._
@@ -98,7 +98,7 @@ Fields define the schema of the Global. To learn more, go to the [Fields](../fie
## Admin Options
The behavior of Globals within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](../custom-components/overview), setting page metadata, and more.
The behavior of Globals within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](../admin/components), setting page metadata, and more.
To configure Admin Options for Globals, use the `admin` property in your Global Config:
@@ -117,17 +117,17 @@ The following options are available:
| Option | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `group` | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this Global from navigation and admin routing. |
| `components` | Swap in your own React components to be used within this Global. [More details](#custom-components). |
| `preview` | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](../admin/preview). |
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
| `meta` | Page metadata overrides to apply to this Global within the Admin Panel. [More details](../admin/metadata). |
| **`group`** | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Global from navigation and admin routing. |
| **`components`** | Swap in your own React components to be used within this Global. [More details](#custom-components). |
| **`preview`** | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](../admin/preview). |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this collection. |
| **`meta`** | Page metadata overrides to apply to this Global within the Admin Panel. [More details](../admin/metadata). |
### Custom Components
Globals can set their own [Custom Components](../custom-components/overview) which only apply to Global-specific UI within the [Admin Panel](../admin/overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
Globals can set their own [Custom Components](../admin/components) which only apply to Global-specific UI within the [Admin Panel](../admin/overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
To override Global Components, use the `admin.components` property in your Global Config:
@@ -146,42 +146,17 @@ export const MyGlobal: SanitizedGlobalConfig = {
The following options are available:
#### General
| Option | Description |
| Path | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `elements` | Override or create new elements within the Edit View. [More details](#edit-view-options). |
| `views` | Override or create new views within the Admin Panel. [More details](../custom-components/custom-views). |
#### Edit View Options
```ts
import type { SanitizedGlobalConfig } from 'payload'
export const MyGlobal: SanitizedGlobalConfig = {
// ...
admin: {
components: {
elements: { // highlight-line
// ...
},
},
},
}
```
The following options are available:
| Option | Description |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SaveButton` | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#save-button). |
| `SaveDraftButton` | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#save-draft-button). |
| `PublishButton` | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publish-button). |
| `PreviewButton` | Replace the default Preview Button with a Custom Component. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#preview-button). |
| **`elements.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
| **`elements.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
| **`elements.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
| **`elements.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](../admin/preview) must be enabled. |
| **`views`** | Override or create new views within the Admin Panel. [More details](../admin/views). |
<Banner type="success">
**Note:**
For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).
</Banner>
## GraphQL
@@ -192,9 +167,9 @@ You can also pass an object to the global's `graphQL` property, which allows you
| Option | Description |
| ---------------------- | ----------------------------------------------------------------------------------- |
| `name` | Override the name that will be used in GraphQL schema generation. |
| `disableQueries` | Disable all GraphQL queries that correspond to this global by passing `true`. |
| `disableMutations` | Disable all GraphQL mutations that correspond to this global by passing `true`. |
| **`name`** | Override the name that will be used in GraphQL schema generation. |
| **`disableQueries`** | Disable all GraphQL queries that correspond to this global by passing `true`. |
| **`disableMutations`** | Disable all GraphQL mutations that correspond to this global by passing `true`. |
## TypeScript

View File

@@ -55,9 +55,9 @@ The following options are available:
| Option | Description |
| --------------------- | --------------------------------|
| `fallbackLanguage` | The language to fall back to if the user's preferred language is not supported. Default is `'en'`. |
| `translations` | An object containing the translations. The keys are the language codes and the values are the translations. |
| `supportedLanguages` | An object containing the supported languages. The keys are the language codes and the values are the translations. |
| **`fallbackLanguage`** | The language to fall back to if the user's preferred language is not supported. Default is `'en'`. |
| **`translations`** | An object containing the translations. The keys are the language codes and the values are the translations. |
| **`supportedLanguages`** | An object containing the supported languages. The keys are the language codes and the values are the translations. |
## Adding Languages
@@ -227,7 +227,7 @@ export default buildConfig({
})
```
Import the shared translation types to use in your [Custom Component](../custom-components/overview):
Import the shared translation types to use in your [Custom Component](../admin/components):
```ts
// <rootDir>/components/MyComponent.tsx

View File

@@ -77,12 +77,11 @@ export default buildConfig({
The following options are available:
| Option | Description |
|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`locales`** | Array of all the languages that you would like to support. [More details](#locales) |
| **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. |
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. |
| **`filterAvailableLocales`** | A function that is called with the array of `locales` and the `req`, it should return locales to show in admin UI selector. [See more](#filter-available-options). |
| Option | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **`locales`** | Array of all the languages that you would like to support. [More details](#locales) |
| **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. |
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. |
### Locales
@@ -101,35 +100,6 @@ The locale codes do not need to be in any specific format. It's up to you to def
_* An asterisk denotes that a property is required._
#### Filter Available Options
In some projects you may want to filter the available locales shown in the admin UI selector. You can do this by providing a `filterAvailableLocales` function in your Payload Config. This is called on the server side and is passed the array of locales. This means that you can determine what locales are visible in the localizer selection menu at the top of the admin panel. You could do this per user, or implement a function that scopes these to tenants and more. Here is an example using request headers in a multi-tenant application:
```ts
// ... rest of payload config
localization: {
defaultLocale: 'en',
locales: ['en', 'es'],
filterAvailableLocales: async ({ req, locales }) => {
if (getTenantFromCookie(req.headers, 'text')) {
const fullTenant = await req.payload.findByID({
id: getTenantFromCookie(req.headers, 'text') as string,
collection: 'tenants',
req,
})
if (fullTenant && fullTenant.supportedLocales?.length) {
return locales.filter((locale) => {
return fullTenant.supportedLocales?.includes(locale.code as 'en' | 'es')
})
}
}
return locales
},
}
```
Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document.
## Field Localization
Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize.

View File

@@ -87,7 +87,6 @@ The following options are available:
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
| **`debug`** | Enable to expose more detailed error information. |
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
| **`rateLimit`** | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks, etc. [More details](../production/preventing-abuse#rate-limiting-requests). |
@@ -104,7 +103,7 @@ _* An asterisk denotes that a property is required._
<Banner type="warning">
**Note:**
Some properties are removed from the client-side bundle. [More details](../custom-components/overview#accessing-the-payload-config).
Some properties are removed from the client-side bundle. [More details](../admin/components#accessing-the-payload-config).
</Banner>
### Typescript Config

View File

@@ -1,49 +0,0 @@
---
title: Swap in your own React Context providers
label: Custom Providers
order: 30
desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
As you add more and more [Custom Components](./overview) to your [Admin Panel](../admin/overview), you may find it helpful to add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context)(s) to your app. Payload allows you to inject your own context providers where you can export your own custom hooks, etc.
To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
components: {
providers: ['/path/to/MyProvider'], // highlight-line
},
},
})
```
Then build your Custom Provider as follows:
```tsx
'use client'
import React, { createContext, useContext } from 'react'
const MyCustomContext = React.createContext(myCustomValue)
export function MyProvider({ children }: { children: React.ReactNode }) {
return (
<MyCustomContext.Provider value={myCustomValue}>
{children}
</MyCustomContext.Provider>
)
}
export const useMyCustomContext = () => useContext(MyCustomContext)
```
_For details on how to build Custom Components, see [Building Custom Components](./overview#building-custom-components)._
<Banner type="warning">
**Reminder:** React Context exists only within Client Components. This means they must include the `use client` directive at the top of their files and cannot contain server-only code. To use a Server Component here, simply _wrap_ your Client Component with it.
</Banner>

View File

@@ -1,364 +0,0 @@
---
title: Customizing Views
label: Customizing Views
order: 40
desc:
keywords:
---
Views are the individual pages that make up the [Admin Panel](../admin/overview), such as the Dashboard, [List View](./list-view), and [Edit View](./edit-view). One of the most powerful ways to customize the Admin Panel is to create Custom Views. These are [Custom Components](./overview) that can either replace built-in views or be entirely new.
There are four types of views within the Admin Panel:
- [Root Views](#root-views)
- [Collection Views](#collection-views)
- [Global Views](#global-views)
- [Document Views](./document-views)
To swap in your own Custom View, first determine the scope that corresponds to what you are trying to accomplish, consult the list of available components, then [author your React component(s)](#building-custom-views) accordingly.
## Configuration
### Replacing Views
To customize views, use the `admin.components.views` property in your [Payload Config](../configuration/overview). This is an object with keys for each view you want to customize. Each key corresponds to the view you want to customize.
The exact list of available keys depends on the scope of the view you are customizing, depending on whether it's a [Root View](#root-views), [Collection View](#collection-views), or [Global View](#global-views). Regardless of the scope, the principles are the same.
Here is an example of how to swap out a built-in view:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
// highlight-start
dashboard: {
Component: '/path/to/MyCustomDashboard',
}
// highlight-end
}
}
}
})
```
For more granular control, pass a configuration object instead. Payload exposes the following properties for each view:
| Property | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Component` * | Pass in the component path that should be rendered when a user navigates to this route. |
| `path` * | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
| `exact` | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
| `strict` | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
| `sensitive` | When true, will match if the path is case sensitive.|
| `meta` | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
_* An asterisk denotes that a property is required._
### Adding New Views
To add a _new_ view to the [Admin Panel](../admin/overview), simply add your own key to the `views` object. This is true for all view scopes.
New views require at least the `Component` and `path` properties:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
// highlight-start
myCustomView: {
Component: '/path/to/MyCustomView#MyCustomViewComponent',
path: '/my-custom-view',
},
// highlight-end
},
},
},
})
```
<Banner type="warning">
**Note:**
Routes are cascading, so unless explicitly given the `exact` property, they will
match on URLs that simply _start_ with the route's path. This is helpful when creating catch-all
routes in your application. Alternatively, define your nested route _before_ your parent
route.
</Banner>
## Building Custom Views
Custom Views are simply [Custom Components](./overview) rendered at the page-level. Custom Views can either [replace existing views](#replacing-views) or [add entirely new ones](#adding-new-views). The process is generally the same regardless of the type of view you are customizing.
To understand how to build Custom Views, first review the [Building Custom Components](./overview#building-custom-components) guide. Once you have a Custom Component ready, you can use it as a Custom View.
```ts
import type { CollectionConfig } from 'payload'
export const MyCollectionConfig: CollectionConfig = {
// ...
admin: {
components: {
views: {
// highlight-start
edit: {
Component: '/path/to/MyCustomView' // highlight-line
}
// highlight-end
},
},
},
}
```
### Default Props
Your Custom Views will be provided with the following props:
| Prop | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `initPageResult` | An object containing `req`, `payload`, `permissions`, etc. |
| `clientConfig` | The Client Config object. [More details](./overview#accessing-the-payload-config). |
| `importMap` | The import map object. |
| `params` | An object containing the [Dynamic Route Parameters](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes). |
| `searchParams` | An object containing the [Search Parameters](https://developer.mozilla.org/docs/Learn/Common_questions/What_is_a_URL#parameters). |
| `doc` | The document being edited. Only available in Document Views. [More details](./document-views). |
| `i18n` | The [i18n](../configuration/i18n) object. |
| `payload` | The [Payload](../local-api/overview) class. |
<Banner type="warning">
**Note:**
Some views may receive additional props, such as [Collection Views](#collection-views) and [Global Views](#global-views). See the relevant section for more details.
</Banner>
Here is an example of a Custom View component:
```tsx
import type { AdminViewServerProps } from 'payload'
import { Gutter } from '@payloadcms/ui'
import React from 'react'
export function MyCustomView(props: AdminViewServerProps) {
return (
<Gutter>
<h1>Custom Default Root View</h1>
<p>This view uses the Default Template.</p>
</Gutter>
)
}
```
<Banner type="success">
**Tip:**
For consistent layout and navigation, you may want to wrap your Custom View with one of the built-in [Template](./overview#templates).
</Banner>
### View Templates
Your Custom Root Views can optionally use one of the templates that Payload provides. The most common of these is the Default Template which provides the basic layout and navigation.
Here is an example of how to use the Default Template in your Custom View:
```tsx
import type { AdminViewServerProps } from 'payload'
import { DefaultTemplate } from '@payloadcms/next/templates'
import { Gutter } from '@payloadcms/ui'
import React from 'react'
export function MyCustomView({
initPageResult,
params,
searchParams,
}: AdminViewServerProps) {
return (
<DefaultTemplate
i18n={initPageResult.req.i18n}
locale={initPageResult.locale}
params={params}
payload={initPageResult.req.payload}
permissions={initPageResult.permissions}
searchParams={searchParams}
user={initPageResult.req.user || undefined}
visibleEntities={initPageResult.visibleEntities}
>
<Gutter>
<h1>Custom Default Root View</h1>
<p>This view uses the Default Template.</p>
</Gutter>
</DefaultTemplate>
)
}
```
### Securing Custom Views
All Custom Views are public by default. It's up to you to secure your custom views. If your view requires a user to be logged in or to have certain access rights, you should handle that within your view component yourself.
Here is how you might secure a Custom View:
```tsx
import type { AdminViewServerProps } from 'payload'
import { Gutter } from '@payloadcms/ui'
import React from 'react'
export function MyCustomView({
initPageResult
}: AdminViewServerProps) {
const {
req: {
user
}
} = initPageResult
if (!user) {
return <p>You must be logged in to view this page.</p>
}
return (
<Gutter>
<h1>Custom Default Root View</h1>
<p>This view uses the Default Template.</p>
</Gutter>
)
}
```
## Root Views
Root Views are the main views of the [Admin Panel](../admin/overview). These are views that are scoped directly under the `/admin` route, such as the Dashboard or Account views.
To [swap out](#replacing-views) Root Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property at the root of your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
// highlight-start
dashboard: {
Component: '/path/to/Dashboard',
}
// highlight-end
// Other options include:
// - account
// - [key: string]
// See below for more details
},
},
},
})
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
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. |
| `[key]` | Any other key can be used to add a completely new Root View. [More details](#adding-new-views). |
## Collection Views
Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views.
To [swap out](#replacing-views) Collection Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property of your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollectionConfig: CollectionConfig = {
// ...
admin: {
components: {
views: {
// highlight-start
edit: {
default: {
Component: '/path/to/MyCustomCollectionView',
}
}
// highlight-end
// Other options include:
// - list
// - [key: string]
// See below for more details
},
},
},
}
```
<Banner type="success">
**Reminder:**
The `edit` key is comprised of various nested views, known as Document Views, that relate to the same Collection Document. [More details](./document-views).
</Banner>
The following options are available:
| Property | Description |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `edit` | The Edit View corresponds to a single Document for any given Collection and consists of various nested views. [More details](./document-views). |
| `list` | The List View is used to show a list of Documents for any given Collection. [More details](#list-view). |
| `[key]` | Any other key can be used to add a completely new Collection View. [More details](#adding-new-views). |
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
## Global Views
Global Views are views that are scoped under the `/globals` route, such as the Edit View.
To [swap out](#replacing-views) Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../configuration/globals):
```ts
import type { SanitizedGlobalConfig } from 'payload'
export const MyGlobalConfig: SanitizedGlobalConfig = {
// ...
admin: {
components: {
views: {
// highlight-start
edit: {
default: {
Component: '/path/to/MyCustomGlobalView',
}
}
// highlight-end
// Other options include:
// - [key: string]
// See below for more details
},
},
},
}
```
<Banner type="success">
**Reminder:**
The `edit` key is comprised of various nested views, known as Document Views, that relate to the same Global Document. [More details](./document-views).
</Banner>
The following options are available:
| Property | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `edit` | The Edit View represents a single Document for any given Global and consists of various nested views. [More details](./document-views). |
| `[key]` | Any other key can be used to add a completely new Global View. [More details](#adding-new-views). |
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._

View File

@@ -1,186 +0,0 @@
---
title: Document Views
label: Document Views
order: 50
desc:
keywords:
---
Document Views consist of multiple, individual views that together represent any single [Collection](../configuration/collections) or [Global](../configuration/globals) Document. All Document Views and are scoped under the `/collections/:collectionSlug/:id` or the `/globals/:globalSlug` route, respectively.
There are a number of default Document Views, such as the [Edit View](./edit-view) and API View, but you can also create [entirely new views](./custom-views#adding-new-views) as needed. All Document Views share a layout and can be given their own tab-based navigation, if desired.
To customize Document Views, use the `admin.components.views.edit[key]` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollectionOrGlobalConfig: CollectionConfig = {
// ...
admin: {
components: {
views: {
// highlight-start
edit: {
default: {
Component: '/path/to/MyCustomEditView',
},
// Other options include:
// - root
// - api
// - versions
// - version
// - livePreview
// - [key: string]
// See below for more details
},
// highlight-end
},
},
},
}
```
## Config Options
The following options are available:
| Property | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `root` | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. [More details](#document-root). |
| `default` | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. [More details](./edit-view). |
| `versions` | The Versions View is used to navigate the version history of a single document. It is rendered within the "Versions" tab. [More details](../versions/overview). |
| `version` | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](../versions/overview). |
| `api` | The API View is used to display the REST API JSON response for a given document. It is rendered within the "API" tab. |
| `livePreview` | The LivePreview view is used to display the Live Preview interface. It is rendered within the "Live Preview" tab. [More details](../live-preview/overview). |
| `[key]` | Any other key can be used to add a completely new Document View. |
_For details on how to build Custom Views, including all available props, see [Building Custom Views](./custom-views#building-custom-views)._
### Document Root
The Document Root is mounted on the top-level route for a Document. Setting this property will completely take over the entire Document View layout, including the title, [Document Tabs](#ocument-tabs), _and all other nested Document Views_ including the [Edit View](./edit-view), API View, etc.
When setting a Document Root, you are responsible for rendering all necessary components and controls, as no document controls or tabs would be rendered. To replace only the Edit View precisely, use the `edit.default` key instead.
To override the Document Root, use the `views.edit.root` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
slug: 'my-collection',
admin: {
components: {
views: {
edit: {
// highlight-start
root: {
Component: '/path/to/MyCustomRootComponent', // highlight-line
},
// highlight-end
},
},
},
},
}
```
### Edit View
The Edit View is where users interact with individual Collection and Global Documents. This is where they can view, edit, and save their content. the Edit View is keyed under the `default` property in the `views.edit` object.
For more information on customizing the Edit View, see the [Edit View](./edit-view) documentation.
## Document Tabs
Each Document View can be given a tab for navigation, 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 Document View, use the `views.edit.[key].tab` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
slug: 'my-collection',
admin: {
components: {
views: {
edit: {
myCustomTab: {
Component: '/path/to/MyCustomTab',
path: '/my-custom-tab',
// highlight-start
tab: {
Component: '/path/to/MyCustomTabComponent'
}
// highlight-end
},
anotherCustomTab: {
Component: '/path/to/AnotherCustomView',
path: '/another-custom-view',
// highlight-start
tab: {
label: 'Another Custom View',
href: '/another-custom-view',
}
// highlight-end
},
},
},
},
},
}
```
<Banner type="warning">
**Note:**
This applies to _both_ Collections _and_ Globals.
</Banner>
The following options are available for tabs:
| Property | Description |
| ----------- | ----------------------------------------------------------------------------------------------------- |
| `label` | The label to display in the tab. |
| `href` | The URL to navigate to when the tab is clicked. This is optional and defaults to the tab's `path`. |
| `Component` | The component to render in the tab. This can be a Server or Client component. [More details](#tab-components) |
### Tab Components
If changing the label or href is not enough, you can also replace the entire tab component with your own custom component. This can be done by setting the `tab.Component` property to the path of your custom component.
Here is an example of how to scaffold a custom Document Tab:
#### Server Component
```tsx
import React from 'react'
import type { DocumentTabServerProps } from 'payload'
import { Link } from '@payloadcms/ui'
export function MyCustomTabComponent(props: DocumentTabServerProps) {
return (
<Link href="/my-custom-tab">
This is a custom Document Tab (Server)
</Link>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { DocumentTabClientProps } from 'payload'
import { Link } from '@payloadcms/ui'
export function MyCustomTabComponent(props: DocumentTabClientProps) {
return (
<Link href="/my-custom-tab">
This is a custom Document Tab (Client)
</Link>
)
}
```

View File

@@ -1,463 +0,0 @@
---
title: Edit View
label: Edit View
order: 60
desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The Edit View is where users interact with individual [Collection](../collections/overview) and [Global](../globals/overview) Documents within the [Admin Panel](../admin/overview). The Edit View contains the actual form in which submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree.
The Edit View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view.
<Banner type="warning">
**Note:**
The Edit View is one of many [Document Views](./document-views) in the Payload Admin Panel. Each Document View is responsible for a different aspect of the interacting with a single Document.
</Banner>
## Custom Edit View
To swap out the entire Edit View with a [Custom View](./custom-views), use the `views.edit.default` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals):
```tsx
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
edit: {
// highlight-start
default: {
Component: '/path/to/MyCustomEditViewComponent',
},
// highlight-end
}
},
},
},
})
```
Here is an example of a custom Edit View:
#### Server Component
```tsx
import React from 'react'
import type { DocumentViewServerProps } from 'payload'
export function MyCustomServerEditView(props: DocumentViewServerProps) {
return (
<div>
This is a custom Edit View (Server)
</div>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { DocumentViewClientProps } from 'payload'
export function MyCustomClientEditView(props: DocumentViewClientProps) {
return (
<div>
This is a custom Edit View (Client)
</div>
)
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](./custom-views#building-custom-views)._
## Custom Components
In addition to swapping out the entire Edit View with a [Custom View](./custom-views), you can also override individual components. This allows you to customize specific parts of the Edit View without swapping out the entire view.
<Banner type="warning">
**Important:**
Collection and Globals are keyed to a different property in the `admin.components` object have slightly different options. Be sure to use the correct key for the entity you are working with.
</Banner>
#### Collections
To override Edit View components for a Collection, use the `admin.components.edit` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
// highlight-start
edit: {
// ...
},
// highlight-end
},
},
}
```
The following options are available:
| Path | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SaveButton` | A button that saves the current document. [More details](#SaveButton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#SaveDraftButton). |
| `PublishButton` | A button that publishes the current document. [More details](#PublishButton). |
| `PreviewButton` | A button that previews the current document. [More details](#PreviewButton). |
| `Description` | A description of the Collection. [More details](#Description). |
| `Upload` | A file upload component. [More details](#Upload). |
#### Globals
To override Edit View components for Globals, use the `admin.components.elements` property in your [Global Config](../configuration/globals):
```ts
import type { GlobalConfig } from 'payload'
export const MyGlobal: GlobalConfig = {
// ...
admin: {
components: {
// highlight-start
elements: {
// ...
},
// highlight-end
},
},
}
```
The following options are available:
| Path | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SaveButton` | A button that saves the current document. [More details](#SaveButton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#SaveDraftButton). |
| `PublishButton` | A button that publishes the current document. [More details](#PublishButton). |
| `PreviewButton` | A button that previews the current document. [More details](#PreviewButton). |
| `Description` | A description of the Global. [More details](#Description). |
### SaveButton
The `SaveButton` property allows you to render a custom Save Button in the Edit View.
To add a `SaveButton` component, use the `components.edit.SaveButton` property in your [Collection Config](../configuration/collections) or `components.elements.SaveButton` in your [Global Config](../configuration/globals):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
edit: {
// highlight-start
SaveButton: '/path/to/MySaveButton',
// highlight-end
}
},
},
}
```
Here's an example of a custom `SaveButton` component:
#### Server Component
```tsx
import React from 'react'
import { SaveButton } from '@payloadcms/ui'
import type { SaveButtonServerProps } from 'payload'
export function MySaveButton(props: SaveButtonServerProps) {
return (
<SaveButton label="Save" />
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import { SaveButton } from '@payloadcms/ui'
import type { SaveButtonClientProps } from 'payload'
export function MySaveButton(props: SaveButtonClientProps) {
return (
<SaveButton label="Save" />
)
}
```
### SaveDraftButton
The `SaveDraftButton` property allows you to render a custom Save Draft Button in the Edit View.
To add a `SaveDraftButton` component, use the `components.edit.SaveDraftButton` property in your [Collection Config](../configuration/collections) or `components.elements.SaveDraftButton` in your [Global Config](../configuration/globals):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
edit: {
// highlight-start
SaveDraftButton: '/path/to/MySaveDraftButton',
// highlight-end
}
},
},
}
```
Here's an example of a custom `SaveDraftButton` component:
#### Server Component
```tsx
import React from 'react'
import { SaveDraftButton } from '@payloadcms/ui'
import type { SaveDraftButtonServerProps } from 'payload'
export function MySaveDraftButton(props: SaveDraftButtonServerProps) {
return (
<SaveDraftButton />
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import { SaveDraftButton } from '@payloadcms/ui'
import type { SaveDraftButtonClientProps } from 'payload'
export function MySaveDraftButton(props: SaveDraftButtonClientProps) {
return (
<SaveDraftButton />
)
}
```
### PublishButton
The `PublishButton` property allows you to render a custom Publish Button in the Edit View.
To add a `PublishButton` component, use the `components.edit.PublishButton` property in your [Collection Config](../configuration/collections) or `components.elements.PublishButton` in your [Global Config](../configuration/globals):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
edit: {
// highlight-start
PublishButton: '/path/to/MyPublishButton',
// highlight-end
}
},
},
}
```
Here's an example of a custom `PublishButton` component:
#### Server Component
```tsx
import React from 'react'
import { PublishButton } from '@payloadcms/ui'
import type { PublishButtonClientProps } from 'payload'
export function MyPublishButton(props: PublishButtonServerProps) {
return (
<PublishButton label="Publish" />
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import { PublishButton } from '@payloadcms/ui'
import type { PublishButtonClientProps } from 'payload'
export function MyPublishButton(props: PublishButtonClientProps) {
return (
<PublishButton label="Publish" />
)
}
```
### PreviewButton
The `PreviewButton` property allows you to render a custom Preview Button in the Edit View.
To add a `PreviewButton` component, use the `components.edit.PreviewButton` property in your [Collection Config](../configuration/collections) or `components.elements.PreviewButton` in your [Global Config](../configuration/globals):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
edit: {
// highlight-start
PreviewButton: '/path/to/MyPreviewButton',
// highlight-end
}
},
},
}
```
Here's an example of a custom `PreviewButton` component:
#### Server Component
```tsx
import React from 'react'
import { PreviewButton } from '@payloadcms/ui'
import type { PreviewButtonServerProps } from 'payload'
export function MyPreviewButton(props: PreviewButtonServerProps) {
return (
<PreviewButton />
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import { PreviewButton } from '@payloadcms/ui'
import type { PreviewButtonClientProps } from 'payload'
export function MyPreviewButton(props: PreviewButtonClientProps) {
return (
<PreviewButton />
)
}
```
### Description
The `Description` property allows you to render a custom description of the Collection or Global in the Edit View.
To add a `Description` component, use the `components.edit.Description` property in your [Collection Config](../configuration/collections) or `components.elements.Description` in your [Global Config](../configuration/globals):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
// highlight-start
Description: '/path/to/MyDescriptionComponent',
// highlight-end
},
},
}
```
<Banner type="warning">
**Note:**
The `Description` component is shared between the Edit View and the [List View](./list-view).
</Banner>
Here's an example of a custom `Description` component:
#### Server Component
```tsx
import React from 'react'
import type { ViewDescriptionServerProps } from 'payload'
export function MyDescriptionComponent(props: ViewDescriptionServerProps) {
return (
<div>
This is a custom description component (Server)
</div>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { ViewDescriptionClientProps } from 'payload'
export function MyDescriptionComponent(props: ViewDescriptionClientProps) {
return (
<div>
This is a custom description component (Client)
</div>
)
}
```
### Upload
The `Upload` property allows you to render a custom file upload component in the Edit View.
To add an `Upload` component, use the `components.edit.Upload` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
edit: {
// highlight-start
Upload: '/path/to/MyUploadComponent',
// highlight-end
}
},
},
}
```
<Banner type="warning">
**Note:**
The Upload component is only available for Collections.
</Banner>
Here's an example of a custom `Upload` component:
```tsx
import React from 'react'
export function MyUploadComponent() {
return (
<input type="file" />
)
}
```

View File

@@ -1,387 +0,0 @@
---
title: List View
label: List View
order: 70
desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The List View is where users interact with a list of [Collection](../collections/overview) Documents within the [Admin Panel](../admin/overview). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many.
The List View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view.
<Banner type="info">
**Note:**
Only [Collections](../collections/overview) have a List View. [Globals](../globals/overview) do not have a List View as they are single documents.
</Banner>
## Custom List View
To swap out the entire List View with a [Custom View](./custom-views), use the `admin.components.views.list` property in your [Payload Config](../configuration/overview):
```tsx
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
// highlight-start
list: '/path/to/MyCustomListView',
// highlight-end
},
},
},
})
```
Here is an example of a custom List View:
#### Server Component
```tsx
import React from 'react'
import type { ListViewServerProps } from 'payload'
import { DefaultListView } from '@payloadcms/ui'
export function MyCustomServerListView(props: ListViewServerProps) {
return (
<div>
This is a custom List View (Server)
</div>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { ListViewClientProps } from 'payload'
export function MyCustomClientListView(props: ListViewClientProps) {
return (
<div>
This is a custom List View (Client)
</div>
)
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](./custom-views#building-custom-views)._
## Custom Components
In addition to swapping out the entire List View with a [Custom View](./custom-views), you can also override individual components. This allows you to customize specific parts of the List View without swapping out the entire view for your own.
To override List View components for a Collection, use the `admin.components` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
// highlight-start
components: {
// ...
},
// highlight-end
},
}
```
The following options are available:
| Path | Description |
|-----------------------|----------------------------------------------------------------------------------------------------------------------------- |
| `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforeList). |
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforeListTable). |
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterList). |
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterListTable). |
| `Description` | A component to render a description of the Collection. [More details](#Description). |
### beforeList
The `beforeList` property allows you to inject custom components before the list of documents in the List View.
To add `beforeList` components, use the `components.beforeList` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
// highlight-start
beforeList: [
'/path/to/MyBeforeListComponent',
],
// highlight-end
},
},
}
```
Here's an example of a custom `beforeList` component:
#### Server Component
```tsx
import React from 'react'
import type { BeforeListServerProps } from 'payload'
export function MyBeforeListComponent(props: BeforeListServerProps) {
return (
<div>
This is a custom beforeList component (Server)
</div>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { BeforeListClientProps } from 'payload'
export function MyBeforeListComponent(props: BeforeListClientProps) {
return (
<div>
This is a custom beforeList component (Client)
</div>
)
}
```
### beforeListTable
The `beforeListTable` property allows you to inject custom components before the table of documents in the List View.
To add `beforeListTable` components, use the `components.beforeListTable` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
// highlight-start
beforeListTable: [
'/path/to/MyBeforeListTableComponent',
],
// highlight-end
},
},
}
```
Here's an example of a custom `beforeListTable` component:
#### Server Component
```tsx
import React from 'react'
import type { BeforeListTableServerProps } from 'payload'
export function MyBeforeListTableComponent(props: BeforeListTableServerProps) {
return (
<div>
This is a custom beforeListTable component (Server)
</div>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { BeforeListTableClientProps } from 'payload'
export function MyBeforeListTableComponent(props: BeforeListTableClientProps) {
return (
<div>
This is a custom beforeListTable component (Client)
</div>
)
}
```
### afterList
The `afterList` property allows you to inject custom components after the list of documents in the List View.
To add `afterList` components, use the `components.afterList` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
// highlight-start
afterList: [
'/path/to/MyAfterListComponent',
],
// highlight-end
},
},
}
```
Here's an example of a custom `afterList` component:
#### Server Component
```tsx
import React from 'react'
import type { AfterListServerProps } from 'payload'
export function MyAfterListComponent(props: AfterListServerProps) {
return (
<div>
This is a custom afterList component (Server)
</div>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { AfterListClientProps } from 'payload'
export function MyAfterListComponent(props: AfterListClientProps) {
return (
<div>
This is a custom afterList component (Client)
</div>
)
}
```
### afterListTable
The `afterListTable` property allows you to inject custom components after the table of documents in the List View.
To add `afterListTable` components, use the `components.afterListTable` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
// highlight-start
afterListTable: [
'/path/to/MyAfterListTableComponent',
],
// highlight-end
},
},
}
```
Here's an example of a custom `afterListTable` component:
#### Server Component
```tsx
import React from 'react'
import type { AfterListTableServerProps } from 'payload'
export function MyAfterListTableComponent(props: AfterListTableServerProps) {
return (
<div>
This is a custom afterListTable component (Server)
</div>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { AfterListTableClientProps } from 'payload'
export function MyAfterListTableComponent(props: AfterListTableClientProps) {
return (
<div>
This is a custom afterListTable component (Client)
</div>
)
}
```
### Description
The `Description` property allows you to render a custom description of the Collection in the List View.
To add a `Description` component, use the `components.Description` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {
// highlight-start
Description: '/path/to/MyDescriptionComponent',
// highlight-end
},
},
}
```
<Banner type="warning">
**Note:**
The `Description` component is shared between the List View and the [Edit View](./edit-view).
</Banner>
Here's an example of a custom `Description` component:
#### Server Component
```tsx
import React from 'react'
import type { ViewDescriptionServerProps } from 'payload'
export function MyDescriptionComponent(props: ViewDescriptionServerProps) {
return (
<div>
This is a custom Collection description component (Server)
</div>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { ViewDescriptionClientProps } from 'payload'
export function MyDescriptionComponent(props: ViewDescriptionClientProps) {
return (
<div>
This is a custom Collection description component (Client)
</div>
)
}
```

View File

@@ -1,491 +0,0 @@
---
title: Swap in your own React components
label: Overview
order: 10
desc: Fully customize your Admin Panel by swapping in your own React components. Add fields, remove views, update routes and change functions to sculpt your perfect Dashboard.
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The Payload [Admin Panel](../admin/overview) is designed to be as minimal and straightforward as possible to allow for easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your [Payload Config](../configuration/overview).
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default. This enables the use of the [Local API](../local-api/overview) directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control.
<Banner type="success">
**Note:**
Client Components continue to be fully supported. To use Client Components in your app, simply include the `'use client'` directive. Payload will automatically detect and remove all [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) default props before rendering your component. [More details](#client-components).
</Banner>
There are four main types of Custom Components in Payload:
- [Root Components](./root-components)
- [Collection Components](../configuration/collections#custom-components)
- [Global Components](../configuration/globals#custom-components)
- [Field Components](../fields/overview#custom-components)
To swap in your own Custom Component, first determine the scope that corresponds to what you are trying to accomplish, consult the list of available components, then [author your React component(s)](#building-custom-components) accordingly.
## Defining Custom Components
As Payload compiles the Admin Panel, it checks your config for Custom Components. When detected, Payload either replaces its own default component with yours, or if none exists by default, renders yours outright. While there are many places where Custom Components are supported in Payload, each is defined in the same way using [Component Paths](#component-paths).
To add a Custom Component, point to its file path in your Payload Config:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
Button: '/src/components/Logout#MyComponent' // highlight-line
}
}
},
})
```
<Banner type="success">
**Note:**
All Custom Components can be either Server Components or Client Components, depending on the presence of the `'use client'` directive at the top of the file.
</Banner>
### Component Paths
In order to ensure the Payload Config is fully Node.js compatible and as lightweight as possible, components are not directly imported into your config. Instead, they are identified by their file path for the Admin Panel to resolve on its own.
Component Paths, by default, are relative to your project's base directory. This is either your current working directory, or the directory specified in `config.admin.importMap.baseDir`.
Components using named exports are identified either by appending `#` followed by the export name, or using the `exportName` property. If the component is the default export, this can be omitted.
```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: {
importMap: {
baseDir: path.resolve(dirname, 'src'), // highlight-line
},
components: {
logout: {
Button: '/components/Logout#MyComponent' // highlight-line
}
}
},
})
```
In this example, we set the base directory to the `src` directory, and omit the `/src/` part of our component path string.
### Component Config
While Custom Components are usually defined as a string, you can also pass in an object with additional options:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
// highlight-start
Button: {
path: '/src/components/Logout',
exportName: 'MyComponent',
}
// highlight-end
}
}
},
})
```
The following options are available:
| Property | Description |
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
| `clientProps` | Props to be passed to the Custom Components if it's a Client Component. [More details](#custom-props). |
| `exportName` | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
| `path` | File path to the Custom Component. Named exports can be appended to the end of the path, separated by a `#`. |
| `serverProps` | Props to be passed to the Custom Component if it's a Server Component. [More details](#custom-props). |
For details on how to build Custom Components, see [Building Custom Components](#building-custom-components).
### Import Map
In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
The Import Map is automatically regenerated at startup and whenever Hot Module Replacement (HMR) runs, or you can run `payload generate:importmap` to manually regenerate it.
#### Custom Imports
If needed, custom items can be appended onto the Import Map. This is mostly only relevant for plugin authors who need to add a custom import that is not referenced in a known location.
To add a custom import to the Import Map, use the `admin.dependencies` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// ...
dependencies: {
myTestComponent: { // myTestComponent is the key - can be anything
path: '/components/TestComponent.js#TestComponent',
type: 'component',
clientProps: {
test: 'hello',
},
},
},
}
}
```
## Building Custom Components
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default. This enables the use of the [Local API](../local-api/overview) directly on the front-end, among other things.
### Default Props
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](../local-api/overview) class and the [`i18n`](../configuration/i18n) object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself.
Here is an example:
```tsx
import React from 'react'
import type { Payload } from 'payload'
async function MyServerComponent({
payload // highlight-line
}: {
payload: Payload
}) {
const page = await payload.findByID({
collection: 'pages',
id: '123',
})
return (
<p>{page.title}</p>
)
}
```
Each Custom Component receives the following props by default:
| Prop | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `payload` | The [Payload](../local-api/overview) class. |
| `i18n` | The [i18n](../configuration/i18n) object. |
<Banner type="warning">
**Reminder:**
All Custom Components also receive various other props that are specific to the component being rendered. See [Root Components](#root-components), [Collection Components](../configuration/collections#custom-components), [Global Components](../configuration/globals#custom-components), or [Field Components](../fields/overview#custom-components) for a complete list of all default props per component.
</Banner>
### Custom Props
It is also possible to pass custom props to your Custom Components. To do this, you can use either the `clientProps` or `serverProps` properties depending on whether your prop is [serializable](https://react.dev/reference/rsc/use-client#serializable-types), and whether your component is a Server or Client Component.
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: { // highlight-line
components: {
logout: {
Button: {
path: '/src/components/Logout#MyComponent',
clientProps: {
myCustomProp: 'Hello, World!' // highlight-line
},
}
}
}
},
})
```
Here is how your component might receive this prop:
```tsx
import React from 'react'
import { Link } from '@payloadcms/ui'
export function MyComponent({ myCustomProp }: { myCustomProp: string }) {
return (
<Link href="/admin/logout">{myCustomProp}</Link>
)
}
```
### Client Components
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, however, it is possible to use [Client Components](https://react.dev/reference/rsc/use-client) by simply adding the `'use client'` directive at the top of your file. Payload will automatically detect and remove all [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) default props before rendering your component.
```tsx
// highlight-start
'use client'
// highlight-end
import React, { useState } from 'react'
export function MyClientComponent() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
)
}
```
<Banner type="warning">
**Reminder:**
Client Components cannot be passed [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types). If you are rendering your Client Component _from within_ a Server Component, ensure that its props are serializable.
</Banner>
### Accessing the Payload Config
From any Server Component, the [Payload Config](../configuration/overview) can be accessed directly from the `payload` prop:
```tsx
import React from 'react'
export default async function MyServerComponent({
payload: {
config // highlight-line
}
}) {
return (
<Link href={config.serverURL}>
Go Home
</Link>
)
}
```
But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions and more. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/hooks#useconfig) hook:
```tsx
'use client'
import React from 'react'
import { useConfig } from '@payloadcms/ui'
export function MyClientComponent() {
// highlight-start
const { config: { serverURL } } = useConfig()
// highlight-end
return (
<Link href={serverURL}>
Go Home
</Link>
)
}
```
<Banner type="success">
See [Using Hooks](#using-hooks) for more details.
</Banner>
Similarly, all [Field Components](../fields/overview#custom-components) automatically receive their respective Field Config through props.
Within Server Components, this prop is named `field`:
```tsx
import React from 'react'
import type { TextFieldServerComponent } from 'payload'
export const MyClientFieldComponent: TextFieldServerComponent = ({ field: { name } }) => {
return (
<p>
{`This field's name is ${name}`}
</p>
)
}
```
Within Client Components, this prop is named `clientField` because its non-serializable props have been removed:
```tsx
'use client'
import React from 'react'
import type { TextFieldClientComponent } from 'payload'
export const MyClientFieldComponent: TextFieldClientComponent = ({ clientField: { name } }) => {
return (
<p>
{`This field's name is ${name}`}
</p>
)
}
```
### Getting the Current Language
All Custom Components can support language translations to be consistent with Payload's [I18n](../configuration/i18n). This will allow your Custom Components to display the correct language based on the user's preferences.
To do this, first add your translation resources to the [I18n Config](../configuration/i18n). Then from any Server Component, you can translate resources using the `getTranslation` function from `@payloadcms/translations`.
All Server Components automatically receive the `i18n` object as a prop by default:
```tsx
import React from 'react'
import { getTranslation } from '@payloadcms/translations'
export default async function MyServerComponent({ i18n }) {
const translatedTitle = getTranslation(myTranslation, i18n) // highlight-line
return (
<p>{translatedTitle}</p>
)
}
```
The best way to do this within a Client Component is to import the `useTranslation` hook from `@payloadcms/ui`:
```tsx
'use client'
import React from 'react'
import { useTranslation } from '@payloadcms/ui'
export function MyClientComponent() {
const { t, i18n } = useTranslation() // highlight-line
return (
<ul>
<li>{t('namespace1:key', { variable: 'value' })}</li>
<li>{t('namespace2:key', { variable: 'value' })}</li>
<li>{i18n.language}</li>
</ul>
)
}
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
</Banner>
### Getting the Current Locale
All [Custom Views](./custom-views) can support multiple locales to be consistent with Payload's [Localization](../configuration/localization) feature. This can be used to scope API requests, etc.
All Server Components automatically receive the `locale` object as a prop by default:
```tsx
import React from 'react'
export default async function MyServerComponent({ payload, locale }) {
const localizedPage = await payload.findByID({
collection: 'pages',
id: '123',
locale,
})
return (
<p>{localizedPage.title}</p>
)
}
```
The best way to do this within a Client Component is to import the `useLocale` hook from `@payloadcms/ui`:
```tsx
'use client'
import React from 'react'
import { useLocale } from '@payloadcms/ui'
function Greeting() {
const locale = useLocale() // highlight-line
const trans = {
en: 'Hello',
es: 'Hola',
}
return (
<span>{trans[locale.code]}</span>
)
}
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
</Banner>
### Using Hooks
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
```tsx
'use client'
import React from 'react'
import { useDocumentInfo } from '@payloadcms/ui'
export function MyClientComponent() {
const { slug } = useDocumentInfo() // highlight-line
return (
<p>{`Entity slug: ${slug}`}</p>
)
}
```
<Banner type="success">
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
</Banner>
### Adding Styles
Payload has a robust [CSS Library](../admin/customizing-css) that you can use to style your Custom Components to match to Payload's built-in styling. This will ensure that your Custom Components integrate well into the existing design system. This will make it so they automatically adapt to any theme changes that might occur.
To apply custom styles, simply import your own `.css` or `.scss` file into your Custom Component:
```tsx
import './index.scss'
export function MyComponent() {
return (
<div className="my-component">
My Custom Component
</div>
)
}
```
Then to colorize your Custom Component's background, for example, you can use the following CSS:
```scss
.my-component {
background-color: var(--theme-elevation-500);
}
```
Payload also exports its [SCSS](https://sass-lang.com) library for reuse which includes mixins, etc. To use this, simply import it as follows into your `.scss` file:
```scss
@import '~@payloadcms/ui/scss';
.my-component {
@include mid-break {
background-color: var(--theme-elevation-900);
}
}
```
<Banner type="success">
**Note:**
You can also drill into Payload's own component styles, or easily apply global, app-wide CSS. More on that [here](../admin/customizing-css).
</Banner>

View File

@@ -1,485 +0,0 @@
---
title: Root Components
label: Root Components
order: 20
desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Root Components are those that affect the [Admin Panel](../admin/overview) at a high-level, such as the logo or the main nav. You can swap out these components with your own [Custom Components](./overview) to create a completely custom look and feel.
When combined with [Custom CSS](../admin/customizing-css), you can create a truly unique experience for your users, such as white-labeling the Admin Panel to match your brand.
To override Root Components, use the `admin.components` property at the root of your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
// ...
},
// highlight-end
},
})
```
## Config Options
The following options are available:
| Path | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `actions` | An array of Custom Components to be rendered _within_ the header of the Admin Panel, providing additional interactivity and functionality. [More details](#actions). |
| `afterDashboard` | An array of Custom Components to inject into the built-in Dashboard, _after_ the default dashboard contents. [More details](#afterdashboard). |
| `afterLogin` | An array of Custom Components to inject into the built-in Login, _after_ the default login form. [More details](#afterlogin). |
| `afterNavLinks` | An array of Custom Components to inject into the built-in Nav, _after_ the links. [More details](#afternavlinks). |
| `beforeDashboard` | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. [More details](#beforedashboard). |
| `beforeLogin` | An array of Custom Components to inject into the built-in Login, _before_ the default login form. [More details](#beforelogin). |
| `beforeNavLinks` | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. [More details](#beforenavlinks). |
| `graphics.Icon` | The simplified logo used in contexts like the the `Nav` component. [More details](#graphicsicon). |
| `graphics.Logo` | The full logo used in contexts like the `Login` view. [More details](#graphicslogo). |
| `header` | An array of Custom Components to be injected above the Payload header. [More details](#header). |
| `logout.Button` | The button displayed in the sidebar that logs the user out. [More details](#logoutbutton). |
| `Nav` | Contains the sidebar / mobile menu in its entirety. [More details](#nav). |
| `providers` | Custom [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) providers that will wrap the entire Admin Panel. [More details](./custom-providers). |
| `views` | Override or create new views within the Admin Panel. [More details](./custom-views). |
_For details on how to build Custom Components, see [Building Custom Components](./overview#building-custom-components)._
<Banner type="success">
**Note:**
You can also use set [Collection Components](../configuration/collections#custom-components) and [Global Components](../configuration/globals#custom-components) in their respective configs.
</Banner>
## Components
### actions
Actions are rendered within the header of the Admin Panel. Actions are typically used to display buttons that add additional interactivity and functionality, although they can be anything you'd like.
To add an action, use the `actions` property in your `admin.components` config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
actions: [
'/path/to/your/component',
],
},
// highlight-end
},
})
```
Here is an example of a simple Action component:
```tsx
export default function MyCustomAction() {
return (
<button onClick={() => alert('Hello, world!')}>
This is a custom action component
</button>
)
}
```
<Banner type="success">
**Note:**
You can also use add Actions to the [Edit View](./edit-view) and [List View](./list-view) in their respective configs.
</Banner>
### beforeDashboard
The `beforeDashboard` property allows you to inject Custom Components into the built-in Dashboard, before the default dashboard contents.
To add `beforeDashboard` components, use the `admin.components.beforeDashboard` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
beforeDashboard: [
'/path/to/your/component',
],
},
// highlight-end
},
})
```
Here is an example of a simple `beforeDashboard` component:
```tsx
export default function MyBeforeDashboardComponent() {
return (
<div>
This is a custom component injected before the Dashboard.
</div>
)
}
```
### afterDashboard
Similar to `beforeDashboard`, the `afterDashboard` property allows you to inject Custom Components into the built-in Dashboard, _after_ the default dashboard contents.
To add `afterDashboard` components, use the `admin.components.afterDashboard` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
afterDashboard: [
'/path/to/your/component',
],
},
// highlight-end
},
})
```
Here is an example of a simple `afterDashboard` component:
```tsx
export default function MyAfterDashboardComponent() {
return (
<div>
This is a custom component injected after the Dashboard.
</div>
)
}
```
### beforeLogin
The `beforeLogin` property allows you to inject Custom Components into the built-in Login view, _before_ the default login form.
To add `beforeLogin` components, use the `admin.components.beforeLogin` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
beforeLogin: [
'/path/to/your/component',
],
},
// highlight-end
},
})
```
Here is an example of a simple `beforeLogin` component:
```tsx
export default function MyBeforeLoginComponent() {
return (
<div>
This is a custom component injected before the Login form.
</div>
)
}
```
### afterLogin
Similar to `beforeLogin`, the `afterLogin` property allows you to inject Custom Components into the built-in Login view, _after_ the default login form.
To add `afterLogin` components, use the `admin.components.afterLogin` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
afterLogin: [
'/path/to/your/component',
],
},
// highlight-end
},
})
```
Here is an example of a simple `afterLogin` component:
```tsx
export default function MyAfterLoginComponent() {
return (
<div>
This is a custom component injected after the Login form.
</div>
)
}
```
### beforeNavLinks
The `beforeNavLinks` property allows you to inject Custom Components into the built-in [Nav Component](#nav), _before_ the nav links themselves.
To add `beforeNavLinks` components, use the `admin.components.beforeNavLinks` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
beforeNavLinks: [
'/path/to/your/component',
],
},
// highlight-end
},
})
```
Here is an example of a simple `beforeNavLinks` component:
```tsx
export default function MyBeforeNavLinksComponent() {
return (
<div>
This is a custom component injected before the Nav links.
</div>
)
}
```
### afterNavLinks
Similar to `beforeNavLinks`, the `afterNavLinks` property allows you to inject Custom Components into the built-in Nav, _after_ the nav links.
To add `afterNavLinks` components, use the `admin.components.afterNavLinks` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
afterNavLinks: [
'/path/to/your/component',
],
},
// highlight-end
},
})
```
Here is an example of a simple `afterNavLinks` component:
```tsx
export default function MyAfterNavLinksComponent() {
return (
<p>This is a custom component injected after the Nav links.</p>
)
}
```
### Nav
The `Nav` property contains the sidebar / mobile menu in its entirety. Use this property to completely replace the built-in Nav with your own custom navigation.
To add a custom nav, use the `admin.components.Nav` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
Nav: '/path/to/your/component',
},
// highlight-end
},
})
```
Here is an example of a simple `Nav` component:
```tsx
import { Link } from '@payloadcms/ui'
export default function MyCustomNav() {
return (
<nav>
<ul>
<li>
<Link href="/dashboard">
Dashboard
</Link>
</li>
</ul>
</nav>
)
}
```
### graphics.Icon
The `Icon` property is the simplified logo used in contexts like the `Nav` component. This is typically a small, square icon that represents your brand.
To add a custom icon, use the `admin.components.graphics.Icon` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
graphics: {
Icon: '/path/to/your/component',
},
},
// highlight-end
},
})
```
Here is an example of a simple `Icon` component:
```tsx
export default function MyCustomIcon() {
return (
<img src="/path/to/your/icon.png" alt="My Custom Icon" />
)
}
```
### graphics.Logo
The `Logo` property is the full logo used in contexts like the `Login` view. This is typically a larger, more detailed representation of your brand.
To add a custom logo, use the `admin.components.graphic.Logo` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
graphics: {
Logo: '/path/to/your/component',
},
},
// highlight-end
},
})
```
Here is an example of a simple `Logo` component:
```tsx
export default function MyCustomLogo() {
return (
<img src="/path/to/your/logo.png" alt="My Custom Logo" />
)
}
```
### Header
The `Header` property allows you to inject Custom Components above the Payload header.
Examples of a custom header components might include an announcements banner, a notifications bar, or anything else you'd like to display at the top of the Admin Panel in a prominent location.
To add `Header` components, use the `admin.components.header` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
Header: [
'/path/to/your/component'
],
},
// highlight-end
},
})
```
Here is an example of a simple `Header` component:
```tsx
export default function MyCustomHeader() {
return (
<header>
<h1>My Custom Header</h1>
</header>
)
}
```
### logout.Button
The `logout.Button` property is the button displayed in the sidebar that should log the user out when clicked.
To add a custom logout button, use the `admin.components.logout.Button` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
logout: {
Button: '/path/to/your/component',
}
},
// highlight-end
},
})
```
Here is an example of a simple `logout.Button` component:
```tsx
export default function MyCustomLogoutButton() {
return (
<button onClick={() => alert('Logging out!')}>
Log Out
</button>
)
}
```

View File

@@ -295,70 +295,6 @@ export const CustomBlocksFieldLabelClient: BlocksFieldLabelClientComponent = ({
}
```
## Block References
If you have multiple blocks used in multiple places, your Payload Config can grow in size, potentially sending more data to the client and requiring more processing on the server. However, you can optimize performance by defining each block **once** in your Payload Config and then referencing its slug wherever it's used instead of passing the entire block config.
To do this, define the block in the `blocks` array of the Payload Config. Then, in the Blocks Field, pass the block slug to the `blockReferences` array - leaving the `blocks` array empty for compatibility reasons.
```ts
import { buildConfig } from 'payload'
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical'
// Payload Config
const config = buildConfig({
// Define the block once
blocks: [
{
slug: 'TextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
collections: [
{
slug: 'collection1',
fields: [
{
name: 'content',
type: 'blocks',
// Reference the block by slug
blockReferences: ['TextBlock'],
blocks: [], // Required to be empty, for compatibility reasons
},
],
},
{
slug: 'collection2',
fields: [
{
name: 'editor',
type: 'richText',
editor: lexicalEditor({
BlocksFeature({
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
blocks: ['TextBlock'],
})
})
},
],
},
],
})
```
<Banner type="warning">
**Reminder:**
Blocks referenced in the `blockReferences` array are treated as isolated from the collection / global config. This has the following implications:
1. The block config cannot be modified or extended in the collection config. It will be identical everywhere it's referenced.
2. Access control for blocks referenced in the `blockReferences` are run only once - data from the collection will not be available in the block's access control.
</Banner>
## TypeScript
As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do so, you can import and use Payload's `Block` type:

View File

@@ -121,22 +121,22 @@ powerful Admin UI.
## Config Options
| Option | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** * | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
| **`collection`** * | The `slug`s having the relationship field or an array of collection slugs. |
| **`on`** * | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If `collection` is an array, this field must exist for all specified collections |
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](../queries/depth#max-depth). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
| **`admin`** | Admin-specific configuration. [More details](#admin-config-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
| Option | Description |
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** * | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
| **`collection`** * | The `slug`s having the relationship field. |
| **`on`** * | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](../queries/depth#max-depth). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
| **`admin`** | Admin-specific configuration. [More details](#admin-config-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
_* An asterisk denotes that a property is required._
@@ -177,35 +177,6 @@ object with:
}
```
## Join Field Data (polymorphic)
When a document is returned that for a polymorphic Join field (with `collection` as an array) is populated with related documents. The structure returned is an
object with:
- `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents
```json
{
"id": "66e3431a3f23e684075aae9c",
"relatedPosts": {
"docs": [
{
"relationTo": "posts",
"value": {
"id": "66e3431a3f23e684075aaeb9",
// other fields...
"category": "66e3431a3f23e684075aae9c"
}
}
// { ... }
],
"hasNextPage": false
}
// other fields...
}
```
## Query Options
The Join Field supports custom queries to filter, sort, and limit the related documents that will be returned. In
@@ -227,8 +198,7 @@ These can be applied to the local API, GraphQL, and REST API.
By adding `joins` to the local API you can customize the request for each join field by the `name` of the field.
```js
const result = await payload.find({
collection: 'categories',
const result = await db.findOne('categories', {
where: {
title: {
equals: 'My Category'
@@ -248,25 +218,6 @@ const result = await payload.find({
})
```
<Banner type="warning">
Currently, `Where` query support on joined documents for join fields with an array of `collection` is limited and not supported for fields inside arrays and blocks.
</Banner>
<Banner type="warning">
Currently, querying by the Join Field itself is not supported, meaning:
```ts
payload.find({
collection: 'categories',
where: {
'relatedPosts.title': { // relatedPosts is a join field
equals: "post"
}
}
})
```
does not work yet.
</Banner>
### Rest API
The rest API supports the same query options as the local API. You can use the `joins` query parameter to customize the

View File

@@ -104,7 +104,7 @@ Here are the available Virtual Fields:
- [Join](../fields/join) - achieves two-way data binding between fields
<Banner type="success">
**Tip:** Don't see a built-in field type that you need? Build it! Using a combination of [Field Validations](#validation) and [Custom Components](../custom-components/overview), you can override the entirety of how a component functions within the [Admin Panel](../admin/overview) to effectively create your own field type.
**Tip:** Don't see a built-in field type that you need? Build it! Using a combination of [Field Validations](#validation) and [Custom Components](../admin/components), you can override the entirety of how a component functions within the [Admin Panel](../admin/overview) to effectively create your own field type.
</Banner>
## Field Options
@@ -429,7 +429,7 @@ The following options are available:
| Option | Description |
| --- | --- |
| **`condition`** | Programmatically show / hide fields based on other fields. [More details](#conditional-logic). |
| **`components`** | All Field Components can be swapped out for [Custom Components](../custom-components/overview) that you define. |
| **`components`** | All Field Components can be swapped out for [Custom Components](../admin/components) that you define. |
| **`description`** | Helper text to display alongside the field to provide more information for the editor. [More details](#description). |
| **`position`** | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. |
| **`width`** | Restrict the width of a field. You can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. |
@@ -455,9 +455,9 @@ A description can be configured in three ways:
To add a Custom Description to a field, use the `admin.description` property in your Field Config:
```ts
import type { CollectionConfig } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: CollectionConfig = {
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
fields: [
// ...
@@ -473,7 +473,7 @@ export const MyCollectionConfig: CollectionConfig = {
```
<Banner type="warning">
**Reminder:** To replace the Field Description with a [Custom Component](../custom-components/overview), use the `admin.components.Description` property. [More details](#description).
**Reminder:** To replace the Field Description with a [Custom Component](../admin/components), use the `admin.components.Description` property. [More details](#description).
</Banner>
#### Description Functions
@@ -483,9 +483,9 @@ Custom Descriptions can also be defined as a function. Description Functions are
To add a Description Function to a field, set the `admin.description` property to a *function* in your Field Config:
```ts
import type { CollectionConfig } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: CollectionConfig = {
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
fields: [
// ...
@@ -618,7 +618,7 @@ export const CollectionConfig: CollectionConfig = {
}
```
*For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#building-custom-components).*
*For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).*
<Banner type="warning">
Instead of replacing the entire Field Component, you can alternately replace or slot-in only specific parts by using the [`Label`](#label), [`Error`](#error), [`beforeInput`](#afterinput-and-beforinput), and [`afterInput`](#afterinput-and-beforinput) properties.
@@ -677,7 +677,7 @@ export const CustomTextField: React.FC = () => {
```
<Banner type="success">
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/hooks) documentation. For additional help, see [Building Custom Components](../admin/components#building-custom-components).
</Banner>
##### TypeScript#field-component-types
@@ -723,7 +723,7 @@ All Cell Components receive the same [Default Field Component Props](#field), pl
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
| **`onClick`** | A function that is called when the cell is clicked. |
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build Custom Components themselves, see [Building Custom Components](../admin/components#building-custom-components).
#### Filter
@@ -747,7 +747,7 @@ export const myField: Field = {
All Custom Filter Components receive the same [Default Field Component Props](#field).
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build Custom Components themselves, see [Building Custom Components](../admin/components#building-custom-components).
#### Label
@@ -771,7 +771,7 @@ export const myField: Field = {
All Custom Label Components receive the same [Default Field Component Props](#field).
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build Custom Components themselves, see [Building Custom Components](../admin/components#building-custom-components).
##### TypeScript#label-component-types
@@ -787,14 +787,14 @@ import type {
#### Description
Alternatively to the [Description Property](#field-descriptions), you can also use a [Custom Component](../custom-components/overview) as the Field Description. This can be useful when you need to provide more complex feedback to the user, such as rendering dynamic field values or other interactive elements.
Alternatively to the [Description Property](#field-descriptions), you can also use a [Custom Component](../admin/components) as the Field Description. This can be useful when you need to provide more complex feedback to the user, such as rendering dynamic field values or other interactive elements.
To add a Description Component to a field, use the `admin.components.Description` property in your Field Config:
```ts
import type { CollectionConfig } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: CollectionConfig = {
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
fields: [
// ...
@@ -813,7 +813,7 @@ export const MyCollectionConfig: CollectionConfig = {
All Custom Description Components receive the same [Default Field Component Props](#field).
For details on how to build a Custom Components themselves, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build a Custom Components themselves, see [Building Custom Components](../admin/components#building-custom-components).
##### TypeScript#description-component-types
@@ -849,7 +849,7 @@ export const myField: Field = {
All Error Components receive the [Default Field Component Props](#field).
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build Custom Components themselves, see [Building Custom Components](../admin/components#building-custom-components).
##### TypeScript#error-component-types
@@ -885,7 +885,7 @@ export const myField: Field = {
All Error Components receive the [Default Field Component Props](#field).
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build Custom Components themselves, see [Building Custom Components](../admin/components#building-custom-components).
##### TypeScript#diff-component-types
@@ -906,9 +906,9 @@ With these properties you can add multiple components *before* and *after* the i
To add components before and after the input element, use the `admin.components.beforeInput` and `admin.components.afterInput` properties in your Field Config:
```ts
import type { CollectionConfig } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: CollectionConfig = {
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
fields: [
// ...
@@ -930,7 +930,7 @@ export const MyCollectionConfig: CollectionConfig = {
All `afterInput` and `beforeInput` Components receive the same [Default Field Component Props](#field).
For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#building-custom-components).
For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).
## TypeScript
@@ -938,4 +938,4 @@ You can import the Payload `Field` type as well as other common types from the `
```ts
import type { Field } from 'payload'
```
```

View File

@@ -345,7 +345,8 @@ const result = await payload.login({
email: 'dev@payloadcms.com',
password: 'rip',
},
req: req, // optional, pass a Request object to be provided to all hooks
req: req, // pass a Request object to be provided to all hooks
res: res, // used to automatically set an HTTP-only auth cookie
depth: 2,
locale: 'en',
fallbackLocale: false,
@@ -383,7 +384,8 @@ const result = await payload.resetPassword({
password: req.body.password, // the new password to set
token: 'afh3o2jf2p3f...', // the token generated from the forgotPassword operation
},
req: req, // optional, pass a Request object to be provided to all hooks
req: req, // pass a Request object to be provided to all hooks
res: res, // used to automatically set an HTTP-only auth cookie
})
```
@@ -397,7 +399,7 @@ const result = await payload.unlock({
// required
email: 'dev@payloadcms.com',
},
req: req, // optional, pass a Request object to be provided to all hooks
req: req, // pass a Request object to be provided to all hooks
overrideAccess: true,
})
```

View File

@@ -443,7 +443,7 @@ For more details, see the [Documentation](https://payloadcms.com/docs/getting-st
})
```
For more details, see the [Documentation](https://payloadcms.com/docs/custom-components/overview#component-paths).
For more details, see the [Documentation](https://payloadcms.com/docs/admin/components#component-paths).
1. All Custom Components are now server-rendered by default, and therefore, cannot use state or hooks directly. If youre using Custom Components in your app that requires state or hooks, add the `'use client'` directive at the top of the file.
@@ -463,7 +463,7 @@ For more details, see the [Documentation](https://payloadcms.com/docs/getting-st
}
```
For more details, see the [Documentation](https://payloadcms.com/docs/custom-components/overview#client-components).
For more details, see the [Documentation](https://payloadcms.com/docs/admin/components#client-components).
1. The `admin.description` property within Collection, Globals, and Fields no longer accepts a React Component. Instead, you must define it as a Custom Component.
@@ -854,7 +854,7 @@ For more details, see the [Documentation](https://payloadcms.com/docs/getting-st
}
```
For more details, see the [Documentation](https://payloadcms.com/docs/admin/custom-components/overview#accessing-the-payload-config).
For more details, see the [Documentation](https://payloadcms.com/docs/admin/components#accessing-the-payload-config).
1. The `useCollapsible` hook has had slight changes to its property names. `collapsed` is now `isCollapsed` and `withinCollapsible` is now `isWithinCollapsible`.

View File

@@ -101,7 +101,8 @@ unsupported `$facet` aggregation.
### CosmosDB
When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all
fields that may be sorted in the admin UI use the [indexSortableFields](/docs/configuration/overview) option.
fields that may be sorted in the admin UI use the <a href="/docs/configuration/overview">indexSortableFields</a>
configuration option.
## File storage

View File

@@ -51,10 +51,6 @@ const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
// myTextBlock is the slug of the block
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
},
inlineBlocks: {
// myInlineBlock is the slug of the block
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
},
})
export const MyComponent = ({ lexicalData }) => {

View File

@@ -409,7 +409,7 @@ Explore the APIs available through ClientFeature to add the specific functionali
### Adding a client feature to the server feature
Inside of your server feature, you can provide an [import path](/docs/admin/custom-components/overview#component-paths) to the client feature like this:
Inside of your server feature, you can provide an [import path](/docs/admin/components#component-paths) to the client feature like this:
```ts
import { createServerFeature } from '@payloadcms/richtext-lexical';
@@ -614,7 +614,7 @@ import {
COMMAND_PRIORITY_EDITOR
} from '@payloadcms/richtext-lexical/lexical'
import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext'
import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext.js'
import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils'
import { useEffect } from 'react'

View File

@@ -6,7 +6,7 @@ desc: Generate your own TypeScript interfaces based on your collections and glob
keywords: headless cms, typescript, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
While building your own custom functionality into Payload, like [Plugins](../plugins/overview), [Hooks](../hooks/overview), [Access Control](../access-control/overview) functions, [Custom Views](../custom-components/custom-views), [GraphQL queries / mutations](../graphql/overview), or anything else, you may benefit from generating your own TypeScript types dynamically from your Payload Config itself.
While building your own custom functionality into Payload, like [Plugins](../plugins/overview), [Hooks](../hooks/overview), [Access Control](../access-control/overview) functions, [Custom Views](../admin/views), [GraphQL queries / mutations](../graphql/overview), or anything else, you may benefit from generating your own TypeScript types dynamically from your Payload Config itself.
## Types generation script

View File

@@ -20,7 +20,7 @@ It's also possible to set up a TypeScript project from scratch. We plan to write
## Using Payload's Exported Types
Payload exports a number of types that you may find useful while writing your own custom functionality like [Plugins](../plugins/overview), [Hooks](../hooks/overview), [Access Control](../access-control/overview) functions, [Custom Views](../custom-components/custom-views), [GraphQL queries / mutations](../graphql/overview) or anything else.
Payload exports a number of types that you may find useful while writing your own custom functionality like [Plugins](../plugins/overview), [Hooks](../hooks/overview), [Access Control](../access-control/overview) functions, [Custom Views](../admin/views), [GraphQL queries / mutations](../graphql/overview) or anything else.
## Config Types

View File

@@ -88,29 +88,27 @@ export const Media: CollectionConfig = {
_An asterisk denotes that an option is required._
| Option | Description |
|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| Option | Description |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`pasteURL`** | Controls whether files can be uploaded from remote URLs by pasting them into the Upload field. **Enabled by default.** Accepts `false` to disable or an object with an `allowList` of valid remote URLs. [More](#uploading-files-from-remote-urls) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`staticDir`** | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. Defaults to your collection slug |
| **`trimOptions`** | An object passed to the the Sharp image library to trim the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize#trim) |
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
| **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. |
| **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`staticDir`** | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. Defaults to your collection slug |
| **`trimOptions`** | An object passed to the the Sharp image library to trim the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize#trim) |
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
### Payload-wide Upload Options

View File

@@ -23,7 +23,6 @@ export const defaultESLintIgnores = [
'next-env.d.ts',
'**/app',
'src/**/*.spec.ts',
'**/jest.setup.js',
]
/** @typedef {import('eslint').Linter.Config} Config */

View File

@@ -13,7 +13,7 @@ export interface Props {
onLoad?: () => void
priority?: boolean // for NextImage only
ref?: Ref<HTMLImageElement | HTMLVideoElement | null>
resource?: MediaType | string | number | null // for Payload media
resource?: MediaType | string | number // for Payload media
size?: string // for NextImage only
src?: StaticImageData // for static media
videoClassName?: string

View File

@@ -10,9 +10,6 @@ import { setCookieBasedOnDomain } from './hooks/setCookieBasedOnDomain'
import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields'
const defaultTenantArrayField = tenantsArrayField({
tenantsArrayFieldName: 'tenants',
tenantsArrayTenantFieldName: 'tenant',
tenantsCollectionSlug: 'tenants',
arrayFieldAccess: {},
tenantFieldAccess: {},
rowFields: [

View File

@@ -27,8 +27,6 @@ const config = withBundleAnalyzer(
env: {
PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname),
// @todo remove in 4.0 - will behave like this by default in 4.0
PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true',
},
async redirects() {
return [

View File

@@ -5,7 +5,6 @@ import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getSession } from './utilities/getSession.js'
export const count: Count = async function count(
@@ -24,11 +23,9 @@ export const count: Count = async function count(
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})

View File

@@ -1,11 +1,10 @@
import type { CountOptions } from 'mongodb'
import type { CountGlobalVersions } from 'payload'
import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getSession } from './utilities/getSession.js'
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
@@ -24,14 +23,9 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const query = await buildQuery({
adapter: this,
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.globals.config.find((each) => each.slug === global),
true,
),
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})

View File

@@ -1,11 +1,10 @@
import type { CountOptions } from 'mongodb'
import type { CountVersions } from 'payload'
import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getSession } from './utilities/getSession.js'
export const countVersions: CountVersions = async function countVersions(
@@ -24,14 +23,9 @@ export const countVersions: CountVersions = async function countVersions(
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const query = await buildQuery({
adapter: this,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})

View File

@@ -3,7 +3,6 @@ import type { DeleteMany } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getSession } from './utilities/getSession.js'
export const deleteMany: DeleteMany = async function deleteMany(
@@ -15,10 +14,8 @@ export const deleteMany: DeleteMany = async function deleteMany(
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
const query = await Model.buildQuery({
payload: this.payload,
where,
})

View File

@@ -3,7 +3,6 @@ import type { DeleteOne, Document } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
@@ -22,18 +21,12 @@ export const deleteOne: DeleteOne = async function deleteOne(
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
const query = await Model.buildQuery({
payload: this.payload,
where,
})
const doc = await Model.findOneAndDelete(query, options)?.lean()
if (!doc) {
return null
}
const doc = await Model.findOneAndDelete(query, options).lean()
let result: Document = JSON.parse(JSON.stringify(doc))

View File

@@ -1,8 +1,7 @@
import { buildVersionCollectionFields, type DeleteVersions } from 'payload'
import type { DeleteVersions } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { getSession } from './utilities/getSession.js'
export const deleteVersions: DeleteVersions = async function deleteVersions(
@@ -13,14 +12,9 @@ export const deleteVersions: DeleteVersions = async function deleteVersions(
const session = await getSession(this, req)
const query = await buildQuery({
adapter: this,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
const query = await VersionsModel.buildQuery({
locale,
payload: this.payload,
where,
})

View File

@@ -5,7 +5,6 @@ import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
@@ -51,11 +50,9 @@ export const find: Find = async function find(
})
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})

View File

@@ -5,7 +5,6 @@ import { combineQueries } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
@@ -15,22 +14,20 @@ export const findGlobal: FindGlobal = async function findGlobal(
{ slug, locale, req, select, where },
) {
const Model = this.globals
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields
const options: QueryOptions = {
lean: true,
select: buildProjectionFromSelect({
adapter: this,
fields,
fields: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields,
select,
}),
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
fields,
const query = await Model.buildQuery({
globalSlug: slug,
locale,
payload: this.payload,
where: combineQueries({ globalType: { equals: slug } }, where),
})

View File

@@ -5,7 +5,6 @@ import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
@@ -47,10 +46,10 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
})
}
const query = await buildQuery({
adapter: this,
fields: versionFields,
const query = await Model.buildQuery({
globalSlug: global,
locale,
payload: this.payload,
where,
})

View File

@@ -3,7 +3,6 @@ import type { Document, FindOne } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
@@ -21,11 +20,9 @@ export const findOne: FindOne = async function findOne(
session,
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: collectionConfig.flattenedFields,
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})

View File

@@ -5,7 +5,6 @@ import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
@@ -42,12 +41,9 @@ export const findVersions: FindVersions = async function findVersions(
})
}
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
const query = await buildQuery({
adapter: this,
fields,
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})
@@ -62,7 +58,7 @@ export const findVersions: FindVersions = async function findVersions(
pagination,
projection: buildProjectionFromSelect({
adapter: this,
fields,
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
select,
}),
sort,

View File

@@ -12,7 +12,7 @@ import type { CollectionModel } from './types.js'
import { buildCollectionSchema } from './models/buildCollectionSchema.js'
import { buildGlobalModel } from './models/buildGlobalModel.js'
import { buildSchema } from './models/buildSchema.js'
import { getBuildQueryPlugin } from './queries/getBuildQueryPlugin.js'
import { getBuildQueryPlugin } from './queries/buildQuery.js'
import { getDBName } from './utilities/getDBName.js'
export const init: Init = function init(this: MongooseAdapter) {
@@ -26,19 +26,15 @@ export const init: Init = function init(this: MongooseAdapter) {
const versionCollectionFields = buildVersionCollectionFields(this.payload.config, collection)
const versionSchema = buildSchema({
buildSchemaOptions: {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
},
...schemaOptions,
const versionSchema = buildSchema(this.payload, versionCollectionFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
},
configFields: versionCollectionFields,
payload: this.payload,
...schemaOptions,
})
versionSchema.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true }).plugin(
@@ -81,18 +77,14 @@ export const init: Init = function init(this: MongooseAdapter) {
const versionGlobalFields = buildVersionGlobalFields(this.payload.config, global)
const versionSchema = buildSchema({
buildSchemaOptions: {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
},
const versionSchema = buildSchema(this.payload, versionGlobalFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
},
configFields: versionGlobalFields,
payload: this.payload,
})
versionSchema.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true }).plugin(

View File

@@ -4,7 +4,7 @@ import type { Payload, SanitizedCollectionConfig } from 'payload'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
import { buildSchema } from './buildSchema.js'
export const buildCollectionSchema = (
@@ -12,20 +12,14 @@ export const buildCollectionSchema = (
payload: Payload,
schemaOptions = {},
): Schema => {
const schema = buildSchema({
buildSchemaOptions: {
draftsEnabled: Boolean(
typeof collection?.versions === 'object' && collection.versions.drafts,
),
indexSortableFields: payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: collection.timestamps !== false,
...schemaOptions,
},
const schema = buildSchema(payload, collection.fields, {
draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts),
indexSortableFields: payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: collection.timestamps !== false,
...schemaOptions,
},
configFields: collection.fields,
payload,
})
if (Array.isArray(collection.upload.filenameCompoundIndex)) {
@@ -40,14 +34,16 @@ export const buildCollectionSchema = (
schema.index(indexDefinition, { unique: true })
}
if (payload.config.indexSortableFields && collection.timestamps !== false) {
schema.index({ updatedAt: 1 })
schema.index({ createdAt: 1 })
}
schema
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
if (
Object.keys(collection.joins).length > 0 ||
Object.keys(collection.polymorphicJoins).length > 0
) {
if (Object.keys(collection.joins).length > 0) {
schema.plugin(mongooseAggregatePaginate)
}

View File

@@ -4,7 +4,7 @@ import mongoose from 'mongoose'
import type { GlobalModel } from '../types.js'
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
import { buildSchema } from './buildSchema.js'
export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
@@ -19,14 +19,10 @@ export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel
Object.values(payload.config.globals).forEach((globalConfig) => {
const globalSchema = buildSchema({
buildSchemaOptions: {
options: {
minimize: false,
},
const globalSchema = buildSchema(payload, globalConfig.fields, {
options: {
minimize: false,
},
configFields: globalConfig.fields,
payload,
})
Globals.discriminator(globalConfig.slug, globalSchema)
})

View File

@@ -3,6 +3,7 @@ import type { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mon
import mongoose from 'mongoose'
import {
type ArrayField,
type Block,
type BlocksField,
type CheckboxField,
type CodeField,
@@ -31,9 +32,9 @@ import {
} from 'payload'
import {
fieldAffectsData,
fieldIsLocalized,
fieldIsPresentationalOnly,
fieldIsVirtual,
fieldShouldBeLocalized,
tabHasName,
} from 'payload/shared'
@@ -50,7 +51,6 @@ type FieldSchemaGenerator = (
schema: Schema,
config: Payload,
buildSchemaOptions: BuildSchemaOptions,
parentIsLocalized: boolean,
) => void
/**
@@ -62,15 +62,7 @@ const formatDefaultValue = (field: FieldAffectingData) =>
? field.defaultValue
: undefined
const formatBaseSchema = ({
buildSchemaOptions,
field,
parentIsLocalized,
}: {
buildSchemaOptions: BuildSchemaOptions
field: FieldAffectingData
parentIsLocalized: boolean
}) => {
const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => {
const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions
const schema: SchemaTypeOptions<unknown> = {
default: formatDefaultValue(field),
@@ -81,7 +73,7 @@ const formatBaseSchema = ({
if (
schema.unique &&
(fieldShouldBeLocalized({ field, parentIsLocalized }) ||
(field.localized ||
draftsEnabled ||
(fieldAffectsData(field) &&
field.type !== 'group' &&
@@ -102,13 +94,8 @@ const localizeSchema = (
entity: NonPresentationalField | Tab,
schema,
localization: false | SanitizedLocalizationConfig,
parentIsLocalized: boolean,
) => {
if (
fieldShouldBeLocalized({ field: entity, parentIsLocalized }) &&
localization &&
Array.isArray(localization.locales)
) {
if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) {
return {
type: localization.localeCodes.reduce(
(localeSchema, locale) => ({
@@ -125,13 +112,11 @@ const localizeSchema = (
return schema
}
export const buildSchema = (args: {
buildSchemaOptions: BuildSchemaOptions
configFields: Field[]
parentIsLocalized?: boolean
payload: Payload
}): Schema => {
const { buildSchemaOptions = {}, configFields, parentIsLocalized, payload } = args
export const buildSchema = (
payload: Payload,
configFields: Field[],
buildSchemaOptions: BuildSchemaOptions = {},
): Schema => {
const { allowIDField, options } = buildSchemaOptions
let fields = {}
@@ -160,7 +145,7 @@ export const buildSchema = (args: {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]
if (addFieldSchema) {
addFieldSchema(field, schema, payload, buildSchemaOptions, parentIsLocalized)
addFieldSchema(field, schema, payload, buildSchemaOptions)
}
}
})
@@ -169,121 +154,96 @@ export const buildSchema = (args: {
}
const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
array: (field: ArrayField, schema, payload, buildSchemaOptions, parentIsLocalized) => {
array: (
field: ArrayField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
) => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: [
buildSchema({
buildSchemaOptions: {
allowIDField: true,
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
_id: false,
id: false,
minimize: false,
},
buildSchema(payload, field.fields, {
allowIDField: true,
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
_id: false,
id: false,
minimize: false,
},
configFields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
payload,
}),
],
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
blocks: (field: BlocksField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
blocks: (
field: BlocksField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const fieldSchema = {
type: [new mongoose.Schema({}, { _id: false, discriminatorKey: 'blockType' })],
}
schema.add({
[field.name]: localizeSchema(
field,
fieldSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, fieldSchema, payload.config.localization),
})
;(field.blockReferences ?? field.blocks).forEach((blockItem) => {
field.blocks.forEach((blockItem: Block) => {
const blockSchema = new mongoose.Schema({}, { _id: false, id: false })
const block = typeof blockItem === 'string' ? payload.blocks[blockItem] : blockItem
block.fields.forEach((blockField) => {
blockItem.fields.forEach((blockField) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
if (addFieldSchema) {
addFieldSchema(
blockField,
blockSchema,
payload,
buildSchemaOptions,
parentIsLocalized || field.localized,
)
addFieldSchema(blockField, blockSchema, payload, buildSchemaOptions)
}
})
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
if (field.localized && payload.config.localization) {
payload.config.localization.localeCodes.forEach((localeCode) => {
// @ts-expect-error Possible incorrect typing in mongoose types, this works
schema.path(`${field.name}.${localeCode}`).discriminator(block.slug, blockSchema)
schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema)
})
} else {
// @ts-expect-error Possible incorrect typing in mongoose types, this works
schema.path(field.name).discriminator(block.slug, blockSchema)
schema.path(field.name).discriminator(blockItem.slug, blockSchema)
}
})
},
checkbox: (
field: CheckboxField,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: Boolean,
}
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
code: (field: CodeField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: String,
}
code: (
field: CodeField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
collapsible: (
field: CollapsibleField,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.fields.forEach((subField: Field) => {
if (fieldIsVirtual(subField)) {
@@ -293,42 +253,41 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, payload, buildSchemaOptions, parentIsLocalized)
addFieldSchema(subField, schema, payload, buildSchemaOptions)
}
})
},
date: (field: DateField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: Date,
}
date: (
field: DateField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
email: (field: EmailField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: String,
}
email: (
field: EmailField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
group: (field: GroupField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
group: (
field: GroupField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions)
// carry indexSortableFields through to versions if drafts enabled
const indexSortableFields =
@@ -338,63 +297,58 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const baseSchema = {
...formattedBaseSchema,
type: buildSchema({
buildSchemaOptions: {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
_id: false,
id: false,
minimize: false,
},
type: buildSchema(payload, field.fields, {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
_id: false,
id: false,
minimize: false,
},
configFields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
payload,
}),
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
json: (field: JSONField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
json: (
field: JSONField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: mongoose.Schema.Types.Mixed,
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
number: (field: NumberField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
number: (
field: NumberField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: field.hasMany ? [Number] : Number,
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
point: (field: PointField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
point: (
field: PointField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema: SchemaTypeOptions<unknown> = {
type: {
type: String,
@@ -409,21 +363,12 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
required: false,
},
}
if (
buildSchemaOptions.disableUnique &&
field.unique &&
fieldShouldBeLocalized({ field, parentIsLocalized })
) {
if (buildSchemaOptions.disableUnique && field.unique && field.localized) {
baseSchema.coordinates.sparse = true
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
if (field.index === true || field.index === undefined) {
@@ -432,7 +377,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
indexOptions.sparse = true
indexOptions.unique = true
}
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
if (field.localized && payload.config.localization) {
payload.config.localization.locales.forEach((locale) => {
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
})
@@ -441,9 +386,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
}
},
radio: (field: RadioField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
radio: (
field: RadioField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: String,
enum: field.options.map((option) => {
if (typeof option === 'object') {
@@ -454,34 +404,28 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
relationship: (
field: RelationshipField,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
) => {
const hasManyRelations = Array.isArray(field.relationTo)
let schemaToReturn: { [key: string]: any } = {}
const valueType = getRelationshipValueType(field, payload)
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
if (field.localized && payload.config.localization) {
schemaToReturn = {
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
if (hasManyRelations) {
localeSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
@@ -492,7 +436,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else {
localeSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: valueType,
ref: field.relationTo,
}
@@ -509,7 +453,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else if (hasManyRelations) {
schemaToReturn = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
@@ -527,7 +471,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else {
schemaToReturn = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: valueType,
ref: field.relationTo,
}
@@ -546,26 +490,25 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
},
richText: (
field: RichTextField,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: mongoose.Schema.Types.Mixed,
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
row: (field: RowField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
row: (
field: RowField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.fields.forEach((subField: Field) => {
if (fieldIsVirtual(subField)) {
return
@@ -574,13 +517,18 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, payload, buildSchemaOptions, parentIsLocalized)
addFieldSchema(subField, schema, payload, buildSchemaOptions)
}
})
},
select: (field: SelectField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
select: (
field: SelectField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: String,
enum: field.options.map((option) => {
if (typeof option === 'object') {
@@ -599,40 +547,34 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
field,
field.hasMany ? [baseSchema] : baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
tabs: (field: TabsField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
tabs: (
field: TabsField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
if (fieldIsVirtual(tab)) {
return
}
const baseSchema = {
type: buildSchema({
buildSchemaOptions: {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
_id: false,
id: false,
minimize: false,
},
type: buildSchema(payload, tab.fields, {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
_id: false,
id: false,
minimize: false,
},
configFields: tab.fields,
parentIsLocalized: parentIsLocalized || tab.localized,
payload,
}),
}
schema.add({
[tab.name]: localizeSchema(
tab,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[tab.name]: localizeSchema(tab, baseSchema, payload.config.localization),
})
} else {
tab.fields.forEach((subField: Field) => {
@@ -642,68 +584,58 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(
subField,
schema,
payload,
buildSchemaOptions,
parentIsLocalized || tab.localized,
)
addFieldSchema(subField, schema, payload, buildSchemaOptions)
}
})
}
})
},
text: (field: TextField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
text: (
field: TextField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: field.hasMany ? [String] : String,
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
textarea: (
field: TextareaField,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: String,
}
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
})
},
upload: (field: UploadField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
upload: (
field: UploadField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const hasManyRelations = Array.isArray(field.relationTo)
let schemaToReturn: { [key: string]: any } = {}
const valueType = getRelationshipValueType(field, payload)
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
if (field.localized && payload.config.localization) {
schemaToReturn = {
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
if (hasManyRelations) {
localeSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
@@ -714,7 +646,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else {
localeSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: valueType,
ref: field.relationTo,
}
@@ -731,7 +663,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else if (hasManyRelations) {
schemaToReturn = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
@@ -749,7 +681,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else {
schemaToReturn = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
...formatBaseSchema(field, buildSchemaOptions),
type: valueType,
ref: field.relationTo,
}

View File

@@ -13,14 +13,12 @@ const migrateModelWithBatching = async ({
config,
fields,
Model,
parentIsLocalized,
session,
}: {
batchSize: number
config: SanitizedConfig
fields: Field[]
Model: Model<any>
parentIsLocalized: boolean
session: ClientSession
}): Promise<void> => {
let hasNext = true
@@ -49,7 +47,7 @@ const migrateModelWithBatching = async ({
}
for (const doc of docs) {
sanitizeRelationshipIDs({ config, data: doc, fields, parentIsLocalized })
sanitizeRelationshipIDs({ config, data: doc, fields })
}
await Model.collection.bulkWrite(
@@ -82,10 +80,6 @@ const hasRelationshipOrUploadField = ({ fields }: { fields: Field[] }): boolean
if ('blocks' in field) {
for (const block of field.blocks) {
if (typeof block === 'string') {
// Skip - string blocks have been added in v3 and thus don't need to be migrated
continue
}
if (hasRelationshipOrUploadField({ fields: block.fields })) {
return true
}
@@ -126,7 +120,6 @@ export async function migrateRelationshipsV2_V3({
config,
fields: collection.fields,
Model: db.collections[collection.slug],
parentIsLocalized: false,
session,
})
@@ -141,7 +134,6 @@ export async function migrateRelationshipsV2_V3({
config,
fields: buildVersionCollectionFields(config, collection),
Model: db.versions[collection.slug],
parentIsLocalized: false,
session,
})
@@ -167,11 +159,7 @@ export async function migrateRelationshipsV2_V3({
// in case if the global doesn't exist in the database yet (not saved)
if (doc) {
sanitizeRelationshipIDs({
config,
data: doc,
fields: global.fields,
})
sanitizeRelationshipIDs({ config, data: doc, fields: global.fields })
await GlobalsModel.collection.updateOne(
{
@@ -193,7 +181,6 @@ export async function migrateRelationshipsV2_V3({
config,
fields: buildVersionGlobalFields(config, global),
Model: db.versions[global.slug],
parentIsLocalized: false,
session,
})

View File

@@ -7,7 +7,6 @@ export async function buildAndOrConditions({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where,
}: {
@@ -15,7 +14,6 @@ export async function buildAndOrConditions({
fields: FlattenedField[]
globalSlug?: string
locale?: string
parentIsLocalized: boolean
payload: Payload
where: Where[]
}): Promise<Record<string, unknown>[]> {
@@ -31,7 +29,6 @@ export async function buildAndOrConditions({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where: condition,
})

View File

@@ -1,33 +1,62 @@
import type { FlattenedField, Where } from 'payload'
import type { FlattenedField, Payload, Where } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { QueryError } from 'payload'
import { parseParams } from './parseParams.js'
export const buildQuery = async ({
adapter,
collectionSlug,
fields,
globalSlug,
locale,
where,
}: {
adapter: MongooseAdapter
type GetBuildQueryPluginArgs = {
collectionSlug?: string
fields: FlattenedField[]
versionsFields?: FlattenedField[]
}
export type BuildQueryArgs = {
globalSlug?: string
locale?: string
payload: Payload
where: Where
}) => {
const result = await parseParams({
collectionSlug,
fields,
globalSlug,
locale,
parentIsLocalized: false,
payload: adapter.payload,
where,
})
return result
}
// This plugin asynchronously builds a list of Mongoose query constraints
// which can then be used in subsequent Mongoose queries.
export const getBuildQueryPlugin = ({
collectionSlug,
versionsFields,
}: GetBuildQueryPluginArgs = {}) => {
return function buildQueryPlugin(schema) {
const modifiedSchema = schema
async function buildQuery({
globalSlug,
locale,
payload,
where,
}: BuildQueryArgs): Promise<Record<string, unknown>> {
let fields = versionsFields
if (!fields) {
if (globalSlug) {
const globalConfig = payload.globals.config.find(({ slug }) => slug === globalSlug)
fields = globalConfig.flattenedFields
}
if (collectionSlug) {
const collectionConfig = payload.collections[collectionSlug].config
fields = collectionConfig.flattenedFields
}
}
const errors = []
const result = await parseParams({
collectionSlug,
fields,
globalSlug,
locale,
payload,
where,
})
if (errors.length > 0) {
throw new QueryError(errors)
}
return result
}
modifiedSchema.statics.buildQuery = buildQuery
}
}

View File

@@ -30,7 +30,6 @@ export async function buildSearchParam({
incomingPath,
locale,
operator,
parentIsLocalized,
payload,
val,
}: {
@@ -40,7 +39,6 @@ export async function buildSearchParam({
incomingPath: string
locale?: string
operator: string
parentIsLocalized: boolean
payload: Payload
val: unknown
}): Promise<SearchParam> {
@@ -71,7 +69,6 @@ export async function buildSearchParam({
name: 'id',
type: idFieldType,
} as FlattenedField,
parentIsLocalized,
path: '_id',
})
} else {
@@ -81,7 +78,6 @@ export async function buildSearchParam({
globalSlug,
incomingPath: sanitizedPath,
locale,
parentIsLocalized,
payload,
})
}
@@ -93,7 +89,6 @@ export async function buildSearchParam({
hasCustomID,
locale,
operator,
parentIsLocalized,
path,
payload,
val,

View File

@@ -7,7 +7,6 @@ type Args = {
config: SanitizedConfig
fields: FlattenedField[]
locale: string
parentIsLocalized?: boolean
sort: Sort
timestamps: boolean
}
@@ -23,7 +22,6 @@ export const buildSortParam = ({
config,
fields,
locale,
parentIsLocalized,
sort,
timestamps,
}: Args): PaginateOptions['sort'] => {
@@ -57,7 +55,6 @@ export const buildSortParam = ({
config,
fields,
locale,
parentIsLocalized,
segments: sortProperty.split('.'),
})
acc[localizedProperty] = sortDirection

View File

@@ -1,65 +0,0 @@
import type { FlattenedField, Payload, Where } from 'payload'
import { QueryError } from 'payload'
import { parseParams } from './parseParams.js'
type GetBuildQueryPluginArgs = {
collectionSlug?: string
versionsFields?: FlattenedField[]
}
export type BuildQueryArgs = {
globalSlug?: string
locale?: string
payload: Payload
where: Where
}
// This plugin asynchronously builds a list of Mongoose query constraints
// which can then be used in subsequent Mongoose queries.
// Deprecated in favor of using simpler buildQuery directly
export const getBuildQueryPlugin = ({
collectionSlug,
versionsFields,
}: GetBuildQueryPluginArgs = {}) => {
return function buildQueryPlugin(schema) {
const modifiedSchema = schema
async function schemaBuildQuery({
globalSlug,
locale,
payload,
where,
}: BuildQueryArgs): Promise<Record<string, unknown>> {
let fields = versionsFields
if (!fields) {
if (globalSlug) {
const globalConfig = payload.globals.config.find(({ slug }) => slug === globalSlug)
fields = globalConfig.flattenedFields
}
if (collectionSlug) {
const collectionConfig = payload.collections[collectionSlug].config
fields = collectionConfig.flattenedFields
}
}
const errors = []
const result = await parseParams({
collectionSlug,
fields,
globalSlug,
locale,
parentIsLocalized: false,
payload,
where,
})
if (errors.length > 0) {
throw new QueryError(errors)
}
return result
}
modifiedSchema.statics.buildQuery = schemaBuildQuery
}
}

View File

@@ -1,12 +1,11 @@
import type { FlattenedField, SanitizedConfig } from 'payload'
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
type Args = {
config: SanitizedConfig
fields: FlattenedField[]
locale: string
parentIsLocalized: boolean
result?: string
segments: string[]
}
@@ -15,7 +14,6 @@ export const getLocalizedSortProperty = ({
config,
fields,
locale,
parentIsLocalized,
result: incomingResult,
segments: incomingSegments,
}: Args): string => {
@@ -37,11 +35,10 @@ export const getLocalizedSortProperty = ({
if (matchedField && !fieldIsPresentationalOnly(matchedField)) {
let nextFields: FlattenedField[]
let nextParentIsLocalized = parentIsLocalized
const remainingSegments = [...segments]
let localizedSegment = matchedField.name
if (fieldShouldBeLocalized({ field: matchedField, parentIsLocalized })) {
if (matchedField.localized) {
// Check to see if next segment is a locale
if (segments.length > 0) {
const nextSegmentIsLocale = config.localization.localeCodes.includes(remainingSegments[0])
@@ -65,30 +62,21 @@ export const getLocalizedSortProperty = ({
matchedField.type === 'array'
) {
nextFields = matchedField.flattenedFields
if (!nextParentIsLocalized) {
nextParentIsLocalized = matchedField.localized
}
}
if (matchedField.type === 'blocks') {
nextFields = (matchedField.blockReferences ?? matchedField.blocks).reduce(
(flattenedBlockFields, _block) => {
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
const block =
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
return [
...flattenedBlockFields,
...block.flattenedFields.filter(
(blockField) =>
(fieldAffectsData(blockField) &&
blockField.name !== 'blockType' &&
blockField.name !== 'blockName') ||
!fieldAffectsData(blockField),
),
]
},
[],
)
nextFields = matchedField.blocks.reduce((flattenedBlockFields, block) => {
return [
...flattenedBlockFields,
...block.flattenedFields.filter(
(blockField) =>
(fieldAffectsData(blockField) &&
blockField.name !== 'blockType' &&
blockField.name !== 'blockName') ||
!fieldAffectsData(blockField),
),
]
}, [])
}
const result = incomingResult ? `${incomingResult}.${localizedSegment}` : localizedSegment
@@ -98,7 +86,6 @@ export const getLocalizedSortProperty = ({
config,
fields: nextFields,
locale,
parentIsLocalized: nextParentIsLocalized,
result,
segments: remainingSegments,
})

View File

@@ -12,7 +12,6 @@ export async function parseParams({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where,
}: {
@@ -20,7 +19,6 @@ export async function parseParams({
fields: FlattenedField[]
globalSlug?: string
locale: string
parentIsLocalized: boolean
payload: Payload
where: Where
}): Promise<Record<string, unknown>> {
@@ -42,7 +40,6 @@ export async function parseParams({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where: condition,
})
@@ -66,7 +63,6 @@ export async function parseParams({
incomingPath: relationOrPath,
locale,
operator,
parentIsLocalized,
payload,
val: pathOperators[operator],
})

View File

@@ -1,21 +1,13 @@
import type {
FlattenedBlock,
FlattenedBlocksField,
FlattenedField,
Payload,
RelationshipField,
} from 'payload'
import type { FlattenedBlock, FlattenedField, Payload, RelationshipField } from 'payload'
import { Types } from 'mongoose'
import { createArrayFromCommaDelineated } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
type SanitizeQueryValueArgs = {
field: FlattenedField
hasCustomID: boolean
locale?: string
operator: string
parentIsLocalized: boolean
path: string
payload: Payload
val: any
@@ -48,18 +40,14 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
// returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships
const getFieldFromSegments = ({
field,
payload,
segments,
}: {
field: FlattenedBlock | FlattenedField
payload: Payload
segments: string[]
}) => {
if ('blocks' in field || 'blockReferences' in field) {
const _field: FlattenedBlocksField = field as FlattenedBlocksField
for (const _block of _field.blockReferences ?? _field.blocks) {
const block: FlattenedBlock = typeof _block === 'string' ? payload.blocks[_block] : _block
const field = getFieldFromSegments({ field: block, payload, segments })
if ('blocks' in field) {
for (const block of field.blocks) {
const field = getFieldFromSegments({ field: block, segments })
if (field) {
return field
}
@@ -79,7 +67,7 @@ const getFieldFromSegments = ({
}
segments.shift()
return getFieldFromSegments({ field: foundField, payload, segments })
return getFieldFromSegments({ field: foundField, segments })
}
}
}
@@ -89,7 +77,6 @@ export const sanitizeQueryValue = ({
hasCustomID,
locale,
operator,
parentIsLocalized,
path,
payload,
val,
@@ -100,10 +87,11 @@ export const sanitizeQueryValue = ({
} => {
let formattedValue = val
let formattedOperator = operator
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
const segments = path.split('.')
segments.shift()
const foundField = getFieldFromSegments({ field, payload, segments })
const foundField = getFieldFromSegments({ field, segments })
if (foundField) {
field = foundField
@@ -221,11 +209,7 @@ export const sanitizeQueryValue = ({
let localizedPath = path
if (
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
payload.config.localization &&
locale
) {
if (field.localized && payload.config.localization && locale) {
localizedPath = `${path}.${locale}`
}

View File

@@ -5,7 +5,6 @@ import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators }
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
@@ -42,17 +41,15 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
const combinedWhere = combineQueries({ latest: { equals: true } }, where)
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
const versionQuery = await buildQuery({
adapter: this,
fields,
const versionQuery = await VersionModel.buildQuery({
locale,
payload: this.payload,
where: combinedWhere,
})
const projection = buildProjectionFromSelect({
adapter: this,
fields,
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
select,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.

View File

@@ -35,7 +35,7 @@ import type {
UploadField,
} from 'payload'
import type { BuildQueryArgs } from './queries/getBuildQueryPlugin.js'
import type { BuildQueryArgs } from './queries/buildQuery.js'
export interface CollectionModel
extends Model<any>,

View File

@@ -37,10 +37,6 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
if (!result) {
return null
}
result = JSON.parse(JSON.stringify(result))
// custom id type reset

View File

@@ -4,7 +4,6 @@ import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
@@ -27,23 +26,22 @@ export async function updateGlobalVersion<T extends TypeWithID>(
const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
const flattenedFields = buildVersionGlobalFields(this.payload.config, currentGlobal, true)
const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: flattenedFields,
fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true),
select,
}),
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
fields: flattenedFields,
const query = await VersionModel.buildQuery({
locale,
payload: this.payload,
where: whereToUse,
})
@@ -55,10 +53,6 @@ export async function updateGlobalVersion<T extends TypeWithID>(
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
if (!doc) {
return null
}
const result = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken

View File

@@ -3,7 +3,6 @@ import type { UpdateOne } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js'
@@ -29,11 +28,9 @@ export const updateOne: UpdateOne = async function updateOne(
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})
@@ -51,10 +48,6 @@ export const updateOne: UpdateOne = async function updateOne(
handleError({ collection, error, req })
}
if (!result) {
return null
}
result = JSON.parse(JSON.stringify(result))
result.id = result._id
result = sanitizeInternalFields(result)

View File

@@ -4,7 +4,6 @@ import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
@@ -20,28 +19,25 @@ export const updateVersion: UpdateVersion = async function updateVersion(
this.payload.collections[collection].config,
)
const flattenedFields = buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
)
const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: flattenedFields,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
select,
}),
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
fields: flattenedFields,
const query = await VersionModel.buildQuery({
locale,
payload: this.payload,
where: whereToUse,
})
@@ -53,10 +49,6 @@ export const updateVersion: UpdateVersion = async function updateVersion(
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
if (!doc) {
return null
}
const result = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken

View File

@@ -1,17 +1,8 @@
import type { PipelineStage } from 'mongoose'
import type {
CollectionSlug,
FlattenedField,
JoinQuery,
SanitizedCollectionConfig,
Where,
} from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { buildQuery } from '../queries/buildQuery.js'
import { buildSortParam } from '../queries/buildSortParam.js'
type BuildJoinAggregationArgs = {
@@ -40,16 +31,11 @@ export const buildJoinAggregation = async ({
query,
versions,
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
if (
(Object.keys(collectionConfig.joins).length === 0 &&
collectionConfig.polymorphicJoins.length == 0) ||
joins === false
) {
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
return
}
const joinConfig = adapter.payload.collections[collection].config.joins
const polymorphicJoinsConfig = adapter.payload.collections[collection].config.polymorphicJoins
const aggregate: PipelineStage[] = [
{
$sort: { createdAt: -1 },
@@ -68,151 +54,10 @@ export const buildJoinAggregation = async ({
})
}
for (const join of polymorphicJoinsConfig) {
if (projection && !projection[join.joinPath]) {
continue
}
if (joins?.[join.joinPath] === false) {
continue
}
const {
limit: limitJoin = join.field.defaultLimit ?? 10,
page,
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
where: whereJoin,
} = joins?.[join.joinPath] || {}
const aggregatedFields: FlattenedField[] = []
for (const collectionSlug of join.field.collection) {
for (const field of adapter.payload.collections[collectionSlug].config.flattenedFields) {
if (!aggregatedFields.some((eachField) => eachField.name === field.name)) {
aggregatedFields.push(field)
}
}
}
const sort = buildSortParam({
config: adapter.payload.config,
fields: aggregatedFields,
locale,
sort: sortJoin,
timestamps: true,
})
const $match = await buildQuery({
adapter,
fields: aggregatedFields,
locale,
where: whereJoin,
})
const sortProperty = Object.keys(sort)[0]
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
const projectSort = sortProperty !== '_id' && sortProperty !== 'relationTo'
const aliases: string[] = []
const as = join.joinPath
for (const collectionSlug of join.field.collection) {
const alias = `${as}.docs.${collectionSlug}`
aliases.push(alias)
aggregate.push({
$lookup: {
as: alias,
from: adapter.collections[collectionSlug].collection.name,
let: {
root_id_: '$_id',
},
pipeline: [
{
$addFields: {
relationTo: {
$literal: collectionSlug,
},
},
},
{
$match: {
$and: [
{
$expr: {
$eq: [`$${join.field.on}`, '$$root_id_'],
},
},
$match,
],
},
},
{
$sort: {
[sortProperty]: sortDirection,
},
},
{
// Unfortunately, we can't use $skip here because we can lose data, instead we do $slice then
$limit: page ? page * limitJoin : limitJoin,
},
{
$project: {
value: '$_id',
...(projectSort && {
[sortProperty]: 1,
}),
relationTo: 1,
},
},
],
},
})
}
aggregate.push({
$addFields: {
[`${as}.docs`]: {
$concatArrays: aliases.map((alias) => `$${alias}`),
},
},
})
aggregate.push({
$set: {
[`${as}.docs`]: {
$sortArray: {
input: `$${as}.docs`,
sortBy: {
[sortProperty]: sortDirection,
},
},
},
},
})
const sliceValue = page ? [(page - 1) * limitJoin, limitJoin] : [limitJoin]
aggregate.push({
$set: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, ...sliceValue],
},
},
})
aggregate.push({
$addFields: {
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
},
},
})
}
for (const slug of Object.keys(joinConfig)) {
for (const join of joinConfig[slug]) {
const joinModel = adapter.collections[join.field.collection]
if (projection && !projection[join.joinPath]) {
continue
}
@@ -223,17 +68,10 @@ export const buildJoinAggregation = async ({
const {
limit: limitJoin = join.field.defaultLimit ?? 10,
page,
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
where: whereJoin,
} = joins?.[join.joinPath] || {}
if (Array.isArray(join.field.collection)) {
throw new Error('Unreachable')
}
const joinModel = adapter.collections[join.field.collection]
const sort = buildSortParam({
config: adapter.payload.config,
fields: adapter.payload.collections[slug].config.flattenedFields,
@@ -257,12 +95,6 @@ export const buildJoinAggregation = async ({
},
]
if (page) {
pipeline.push({
$skip: (page - 1) * limitJoin,
})
}
if (limitJoin > 0) {
pipeline.push({
$limit: limitJoin + 1,
@@ -316,14 +148,7 @@ export const buildJoinAggregation = async ({
})
} else {
const localeSuffix =
fieldShouldBeLocalized({
field: join.field,
parentIsLocalized: join.parentIsLocalized,
}) &&
adapter.payload.config.localization &&
locale
? `.${locale}`
: ''
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${localeSuffix}`
let foreignField: string

View File

@@ -1,11 +1,6 @@
import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload'
import {
deepCopyObjectSimple,
fieldAffectsData,
fieldShouldBeLocalized,
getSelectMode,
} from 'payload/shared'
import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
@@ -13,18 +8,18 @@ const addFieldToProjection = ({
adapter,
databaseSchemaPath,
field,
parentIsLocalized,
projection,
withinLocalizedField,
}: {
adapter: MongooseAdapter
databaseSchemaPath: string
field: FieldAffectingData
parentIsLocalized: boolean
projection: Record<string, true>
withinLocalizedField: boolean
}) => {
const { config } = adapter.payload
if (parentIsLocalized && config.localization) {
if (withinLocalizedField && config.localization) {
for (const locale of config.localization.localeCodes) {
const localeDatabaseSchemaPath = databaseSchemaPath.replace('<locale>', locale)
projection[`${localeDatabaseSchemaPath}${field.name}`] = true
@@ -38,20 +33,20 @@ const traverseFields = ({
adapter,
databaseSchemaPath = '',
fields,
parentIsLocalized = false,
projection,
select,
selectAllOnCurrentLevel = false,
selectMode,
withinLocalizedField = false,
}: {
adapter: MongooseAdapter
databaseSchemaPath?: string
fields: FlattenedField[]
parentIsLocalized?: boolean
projection: Record<string, true>
select: SelectType
selectAllOnCurrentLevel?: boolean
selectMode: SelectMode
withinLocalizedField?: boolean
}) => {
for (const field of fields) {
if (fieldAffectsData(field)) {
@@ -61,8 +56,8 @@ const traverseFields = ({
adapter,
databaseSchemaPath,
field,
parentIsLocalized,
projection,
withinLocalizedField,
})
continue
}
@@ -78,8 +73,8 @@ const traverseFields = ({
adapter,
databaseSchemaPath,
field,
parentIsLocalized,
projection,
withinLocalizedField,
})
continue
}
@@ -91,12 +86,14 @@ const traverseFields = ({
}
let fieldDatabaseSchemaPath = databaseSchemaPath
let fieldWithinLocalizedField = withinLocalizedField
if (fieldAffectsData(field)) {
fieldDatabaseSchemaPath = `${databaseSchemaPath}${field.name}.`
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
if (field.localized) {
fieldDatabaseSchemaPath = `${fieldDatabaseSchemaPath}<locale>.`
fieldWithinLocalizedField = true
}
}
@@ -114,10 +111,10 @@ const traverseFields = ({
adapter,
databaseSchemaPath: fieldDatabaseSchemaPath,
fields: field.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
projection,
select: fieldSelect,
selectMode,
withinLocalizedField: fieldWithinLocalizedField,
})
break
@@ -126,8 +123,7 @@ const traverseFields = ({
case 'blocks': {
const blocksSelect = select[field.name] as SelectType
for (const _block of field.blockReferences ?? field.blocks) {
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
for (const block of field.blocks) {
if (
(selectMode === 'include' && blocksSelect[block.slug] === true) ||
(selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined')
@@ -136,11 +132,11 @@ const traverseFields = ({
adapter,
databaseSchemaPath: fieldDatabaseSchemaPath,
fields: block.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
projection,
select: {},
selectAllOnCurrentLevel: true,
selectMode: 'include',
withinLocalizedField: fieldWithinLocalizedField,
})
continue
}
@@ -164,10 +160,10 @@ const traverseFields = ({
adapter,
databaseSchemaPath: fieldDatabaseSchemaPath,
fields: block.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
projection,
select: blocksSelect[block.slug] as SelectType,
selectMode: blockSelectMode,
withinLocalizedField: fieldWithinLocalizedField,
})
}

View File

@@ -1,4 +1,4 @@
import { flattenAllFields, type Field, type SanitizedConfig } from 'payload'
import type { Field, SanitizedConfig } from 'payload'
import { Types } from 'mongoose'
@@ -74,28 +74,7 @@ const relsFields: Field[] = [
},
]
const referenceBlockFields: Field[] = [
...relsFields,
{
name: 'group',
type: 'group',
fields: relsFields,
},
{
name: 'array',
type: 'array',
fields: relsFields,
},
]
const config = {
blocks: [
{
slug: 'reference-block',
fields: referenceBlockFields,
flattenedFields: flattenAllFields({ fields: referenceBlockFields }),
},
],
collections: [
{
slug: 'docs',
@@ -158,11 +137,6 @@ const config = {
},
],
},
{
name: 'blockReferences',
type: 'blocks',
blockReferences: ['reference-block'],
},
{
name: 'group',
type: 'group',
@@ -347,14 +321,6 @@ describe('sanitizeRelationshipIDs', () => {
group: { ...relsData },
},
],
blockReferences: [
{
blockType: 'reference-block',
array: [{ ...relsData }],
group: { ...relsData },
...relsData,
},
],
group: {
...relsData,
array: [{ ...relsData }],

View File

@@ -1,14 +1,13 @@
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
import { Types } from 'mongoose'
import { traverseFields } from 'payload'
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
import { APIError, traverseFields } from 'payload'
import { fieldAffectsData } from 'payload/shared'
type Args = {
config: SanitizedConfig
data: Record<string, unknown>
fields: Field[]
parentIsLocalized?: boolean
}
interface RelationObject {
@@ -113,7 +112,6 @@ export const sanitizeRelationshipIDs = ({
config,
data,
fields,
parentIsLocalized,
}: Args): Record<string, unknown> => {
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
if (!ref || typeof ref !== 'object') {
@@ -126,7 +124,7 @@ export const sanitizeRelationshipIDs = ({
}
// handle localized relationships
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
if (config.localization && field.localized) {
const locales = config.localization.locales
const fieldRef = ref[field.name]
if (typeof fieldRef !== 'object') {
@@ -152,14 +150,7 @@ export const sanitizeRelationshipIDs = ({
}
}
traverseFields({
callback: sanitize,
config,
fields,
fillEmpty: false,
parentIsLocalized,
ref: data,
})
traverseFields({ callback: sanitize, fields, fillEmpty: false, ref: data })
return data
}

View File

@@ -9,40 +9,32 @@ export const countDistinct: CountDistinct = async function countDistinct(
this: SQLiteAdapter,
{ db, joins, tableName, where },
) {
// When we don't have any joins - use a simple COUNT(*) query.
if (joins.length === 0) {
const countResult = await db
.select({
count: count(),
})
.from(this.tables[tableName])
.where(where)
return Number(countResult[0].count)
}
const chainedMethods: ChainedMethods = []
joins.forEach(({ condition, table }) => {
// COUNT(DISTINCT id) is slow on large tables, so we only use DISTINCT if we have to
const visitedPaths = new Set([])
let useDistinct = false
joins.forEach(({ condition, queryPath, table }) => {
if (!useDistinct && queryPath) {
if (visitedPaths.has(queryPath)) {
useDistinct = true
} else {
visitedPaths.add(queryPath)
}
}
chainedMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
// When we have any joins, we need to count each individual ID only once.
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
const countResult = await chainMethods({
methods: chainedMethods,
query: db
.select({
count: sql`COUNT(1) OVER()`,
count: useDistinct ? sql`COUNT(DISTINCT ${this.tables[tableName].id})` : count(),
})
.from(this.tables[tableName])
.where(where)
.groupBy(this.tables[tableName].id)
.limit(1),
.where(where),
})
return Number(countResult[0].count)

View File

@@ -1,9 +1,8 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
import { and, asc, desc, eq, or, sql } from 'drizzle-orm'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import { sql } from 'drizzle-orm'
import { fieldIsVirtual } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js'
@@ -11,49 +10,11 @@ import type { Result } from './buildFindManyArgs.js'
import buildQuery from '../queries/buildQuery.js'
import { getTableAlias } from '../queries/getTableAlias.js'
import { operatorMap } from '../queries/operatorMap.js'
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
import { jsonAggBuildObject } from '../utilities/json.js'
import { rawConstraint } from '../utilities/rawConstraint.js'
import { chainMethods } from './chainMethods.js'
const flattenAllWherePaths = (where: Where, paths: string[]) => {
for (const k in where) {
if (['AND', 'OR'].includes(k.toUpperCase())) {
if (Array.isArray(where[k])) {
for (const whereField of where[k]) {
flattenAllWherePaths(whereField, paths)
}
}
} else {
// TODO: explore how to support arrays/relationship querying.
paths.push(k.split('.').join('_'))
}
}
}
const buildSQLWhere = (where: Where, alias: string) => {
for (const k in where) {
if (['AND', 'OR'].includes(k.toUpperCase())) {
if (Array.isArray(where[k])) {
const op = 'AND' === k.toUpperCase() ? and : or
const accumulated = []
for (const whereField of where[k]) {
accumulated.push(buildSQLWhere(whereField, alias))
}
return op(...accumulated)
}
} else {
const payloadOperator = Object.keys(where[k])[0]
const value = where[k][payloadOperator]
return operatorMap[payloadOperator](sql.raw(`"${alias}"."${k.split('.').join('_')}"`), value)
}
}
}
type SQLSelect = SQLiteSelectBase<any, any, any, any>
type TraverseFieldArgs = {
_locales: Result
adapter: DrizzleAdapter
@@ -65,7 +26,6 @@ type TraverseFieldArgs = {
joinQuery: JoinQuery
joins?: BuildQueryJoinAliases
locale?: string
parentIsLocalized?: boolean
path: string
select?: SelectType
selectAllOnCurrentLevel?: boolean
@@ -74,6 +34,7 @@ type TraverseFieldArgs = {
topLevelArgs: Record<string, unknown>
topLevelTableName: string
versions?: boolean
withinLocalizedField?: boolean
withTabledFields: {
numbers?: boolean
rels?: boolean
@@ -92,7 +53,6 @@ export const traverseFields = ({
joinQuery = {},
joins,
locale,
parentIsLocalized = false,
path,
select,
selectAllOnCurrentLevel = false,
@@ -101,6 +61,7 @@ export const traverseFields = ({
topLevelArgs,
topLevelTableName,
versions,
withinLocalizedField = false,
withTabledFields,
}: TraverseFieldArgs) => {
fields.forEach((field) => {
@@ -108,11 +69,6 @@ export const traverseFields = ({
return
}
const isFieldLocalized = fieldShouldBeLocalized({
field,
parentIsLocalized,
})
// handle simple relationship
if (
depth > 0 &&
@@ -120,7 +76,7 @@ export const traverseFields = ({
!field.hasMany &&
typeof field.relationTo === 'string'
) {
if (isFieldLocalized) {
if (field.localized) {
_locales.with[`${path}${field.name}`] = true
} else {
currentArgs.with[`${path}${field.name}`] = true
@@ -196,13 +152,13 @@ export const traverseFields = ({
fields: field.flattenedFields,
joinQuery,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: '',
select: typeof arraySelect === 'object' ? arraySelect : undefined,
selectMode,
tablePath: '',
topLevelArgs,
topLevelTableName,
withinLocalizedField: withinLocalizedField || field.localized,
withTabledFields,
})
@@ -229,8 +185,7 @@ export const traverseFields = ({
}
}
;(field.blockReferences ?? field.blocks).forEach((_block) => {
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
field.blocks.forEach((block) => {
const blockKey = `_blocks_${block.slug}`
let blockSelect: boolean | SelectType | undefined
@@ -307,13 +262,13 @@ export const traverseFields = ({
fields: block.flattenedFields,
joinQuery,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: '',
select: typeof blockSelect === 'object' ? blockSelect : undefined,
selectMode: blockSelectMode,
tablePath: '',
topLevelArgs,
topLevelTableName,
withinLocalizedField: withinLocalizedField || field.localized,
withTabledFields,
})
@@ -349,7 +304,6 @@ export const traverseFields = ({
joinQuery,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${path}${field.name}_`,
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
selectAllOnCurrentLevel:
@@ -361,6 +315,7 @@ export const traverseFields = ({
topLevelArgs,
topLevelTableName,
versions,
withinLocalizedField: withinLocalizedField || field.localized,
withTabledFields,
})
@@ -387,7 +342,6 @@ export const traverseFields = ({
const {
limit: limitArg = field.defaultLimit ?? 10,
page,
sort = field.defaultSort,
where,
} = joinQuery[joinSchemaPath] || {}
@@ -398,231 +352,114 @@ export const traverseFields = ({
limit += 1
}
const fields = adapter.payload.collections[field.collection].config.flattenedFields
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
const joins: BuildQueryJoinAliases = []
const currentIDColumn = versions
? adapter.tables[currentTableName].parent
: adapter.tables[currentTableName].id
let joinQueryWhere: Where
if (Array.isArray(field.targetField.relationTo)) {
joinQueryWhere = {
[field.on]: {
equals: {
relationTo: collectionSlug,
value: rawConstraint(currentIDColumn),
},
},
}
} else {
joinQueryWhere = {
[field.on]: {
equals: rawConstraint(currentIDColumn),
},
}
}
if (where && Object.keys(where).length) {
joinQueryWhere = {
and: [joinQueryWhere, where],
}
}
const columnName = `${path.replaceAll('.', '_')}${field.name}`
const subQueryAlias = `${columnName}_alias`
const { newAliasTable } = getTableAlias({
adapter,
tableName: joinCollectionTableName,
})
const {
orderBy,
selectFields,
where: subQueryWhere,
} = buildQuery({
adapter,
aliasTable: newAliasTable,
fields,
joins,
locale,
selectLocale: true,
sort,
tableName: joinCollectionTableName,
where: joinQueryWhere,
})
const chainedMethods: ChainedMethods = []
joins.forEach(({ type, condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: type ?? 'leftJoin',
})
})
if (limit !== 0) {
chainedMethods.push({
args: [limit],
method: 'limit',
})
}
const db = adapter.drizzle as LibSQLDatabase
if (Array.isArray(field.collection)) {
let currentQuery: null | SQLSelect = null
const onPath = field.on.split('.').join('_')
for (let key in selectFields) {
const val = selectFields[key]
if (Array.isArray(sort)) {
throw new Error('Not implemented')
if (val.table && getNameFromDrizzleTable(val.table) === joinCollectionTableName) {
delete selectFields[key]
key = key.split('.').pop()
selectFields[key] = newAliasTable[key]
}
let sanitizedSort = sort
if (!sanitizedSort) {
if (
field.collection.some((collection) =>
adapter.payload.collections[collection].config.fields.some(
(f) => f.type === 'date' && f.name === 'createdAt',
),
)
) {
sanitizedSort = '-createdAt'
} else {
sanitizedSort = 'id'
}
}
const sortOrder = sanitizedSort.startsWith('-') ? desc : asc
sanitizedSort = sanitizedSort.replace('-', '')
const sortPath = sanitizedSort.split('.').join('_')
const wherePaths: string[] = []
if (where) {
flattenAllWherePaths(where, wherePaths)
}
for (const collection of field.collection) {
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(collection))
const table = adapter.tables[joinCollectionTableName]
const sortColumn = table[sortPath]
const selectFields = {
id: adapter.tables[joinCollectionTableName].id,
parent: sql`${adapter.tables[joinCollectionTableName][onPath]}`.as(onPath),
relationTo: sql`${collection}`.as('relationTo'),
sortPath: sql`${sortColumn ? sortColumn : null}`.as('sortPath'),
}
// Select for WHERE and Fallback NULL
for (const path of wherePaths) {
if (adapter.tables[joinCollectionTableName][path]) {
selectFields[path] = sql`${adapter.tables[joinCollectionTableName][path]}`.as(path)
// Allow to filter by collectionSlug
} else if (path !== 'relationTo') {
selectFields[path] = sql`null`.as(path)
}
}
const query = db.select(selectFields).from(adapter.tables[joinCollectionTableName])
if (currentQuery === null) {
currentQuery = query as unknown as SQLSelect
} else {
currentQuery = currentQuery.unionAll(query) as SQLSelect
}
}
const subQueryAlias = `${columnName}_subquery`
let sqlWhere = eq(
adapter.tables[currentTableName].id,
sql.raw(`"${subQueryAlias}"."${onPath}"`),
)
if (where && Object.keys(where).length > 0) {
sqlWhere = and(sqlWhere, buildSQLWhere(where, subQueryAlias))
}
currentQuery = currentQuery.orderBy(sortOrder(sql`"sortPath"`)) as SQLSelect
if (page && limit !== 0) {
const offset = (page - 1) * limit
if (offset > 0) {
currentQuery = currentQuery.offset(offset) as SQLSelect
}
}
if (limit) {
currentQuery = currentQuery.limit(limit) as SQLSelect
}
currentArgs.extras[columnName] = sql`${db
.select({
id: jsonAggBuildObject(adapter, {
id: sql.raw(`"${subQueryAlias}"."id"`),
relationTo: sql.raw(`"${subQueryAlias}"."relationTo"`),
}),
})
.from(sql`${currentQuery.as(subQueryAlias)}`)
.where(sqlWhere)}`.as(columnName)
} else {
const fields = adapter.payload.collections[field.collection].config.flattenedFields
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
const joins: BuildQueryJoinAliases = []
const currentIDColumn = versions
? adapter.tables[currentTableName].parent
: adapter.tables[currentTableName].id
let joinQueryWhere: Where
if (Array.isArray(field.targetField.relationTo)) {
joinQueryWhere = {
[field.on]: {
equals: {
relationTo: collectionSlug,
value: rawConstraint(currentIDColumn),
},
},
}
} else {
joinQueryWhere = {
[field.on]: {
equals: rawConstraint(currentIDColumn),
},
}
}
if (where && Object.keys(where).length) {
joinQueryWhere = {
and: [joinQueryWhere, where],
}
}
const columnName = `${path.replaceAll('.', '_')}${field.name}`
const subQueryAlias = `${columnName}_alias`
const { newAliasTable } = getTableAlias({
adapter,
tableName: joinCollectionTableName,
})
const {
orderBy,
selectFields,
where: subQueryWhere,
} = buildQuery({
adapter,
aliasTable: newAliasTable,
fields,
joins,
locale,
parentIsLocalized,
selectLocale: true,
sort,
tableName: joinCollectionTableName,
where: joinQueryWhere,
})
const chainedMethods: ChainedMethods = []
joins.forEach(({ type, condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: type ?? 'leftJoin',
})
})
if (page && limit !== 0) {
const offset = (page - 1) * limit - 1
if (offset > 0) {
chainedMethods.push({
args: [offset],
method: 'offset',
})
}
}
if (limit !== 0) {
chainedMethods.push({
args: [limit],
method: 'limit',
})
}
const db = adapter.drizzle as LibSQLDatabase
for (let key in selectFields) {
const val = selectFields[key]
if (val.table && getNameFromDrizzleTable(val.table) === joinCollectionTableName) {
delete selectFields[key]
key = key.split('.').pop()
selectFields[key] = newAliasTable[key]
}
}
const subQuery = chainMethods({
methods: chainedMethods,
query: db
.select(selectFields as any)
.from(newAliasTable)
.where(subQueryWhere)
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
}).as(subQueryAlias)
currentArgs.extras[columnName] = sql`${db
.select({
result: jsonAggBuildObject(adapter, {
id: sql.raw(`"${subQueryAlias}".id`),
...(selectFields._locale && {
locale: sql.raw(`"${subQueryAlias}".${selectFields._locale.name}`),
}),
}),
})
.from(sql`${subQuery}`)}`.as(subQueryAlias)
}
const subQuery = chainMethods({
methods: chainedMethods,
query: db
.select(selectFields as any)
.from(newAliasTable)
.where(subQueryWhere)
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
}).as(subQueryAlias)
currentArgs.extras[columnName] = sql`${db
.select({
result: jsonAggBuildObject(adapter, {
id: sql.raw(`"${subQueryAlias}".id`),
...(selectFields._locale && {
locale: sql.raw(`"${subQueryAlias}".${selectFields._locale.name}`),
}),
}),
})
.from(sql`${subQuery}`)}`.as(subQueryAlias)
break
}
@@ -631,7 +468,7 @@ export const traverseFields = ({
break
}
const args = isFieldLocalized ? _locales : currentArgs
const args = field.localized ? _locales : currentArgs
if (!args.columns) {
args.columns = {}
}
@@ -693,7 +530,7 @@ export const traverseFields = ({
if (select || selectAllOnCurrentLevel) {
const fieldPath = `${path}${field.name}`
if ((isFieldLocalized || parentIsLocalized) && _locales) {
if ((field.localized || withinLocalizedField) && _locales) {
_locales.columns[fieldPath] = true
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
currentArgs.columns[fieldPath] = true
@@ -715,7 +552,7 @@ export const traverseFields = ({
) {
const fieldPath = `${path}${field.name}`
if ((isFieldLocalized || parentIsLocalized) && _locales) {
if ((field.localized || withinLocalizedField) && _locales) {
_locales.columns[fieldPath] = true
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
currentArgs.columns[fieldPath] = true

View File

@@ -1,6 +1,6 @@
import { count, sql } from 'drizzle-orm'
import type { ChainedMethods } from '../types.js'
import type { ChainedMethods, TransactionPg } from '../types.js'
import type { BasePostgresAdapter, CountDistinct } from './types.js'
import { chainMethods } from '../find/chainMethods.js'
@@ -9,40 +9,33 @@ export const countDistinct: CountDistinct = async function countDistinct(
this: BasePostgresAdapter,
{ db, joins, tableName, where },
) {
// When we don't have any joins - use a simple COUNT(*) query.
if (joins.length === 0) {
const countResult = await db
.select({
count: count(),
})
.from(this.tables[tableName])
.where(where)
return Number(countResult[0].count)
}
const chainedMethods: ChainedMethods = []
joins.forEach(({ condition, table }) => {
// COUNT(DISTINCT id) is slow on large tables, so we only use DISTINCT if we have to
const visitedPaths = new Set([])
let useDistinct = false
joins.forEach(({ condition, queryPath, table }) => {
if (!useDistinct && queryPath) {
if (visitedPaths.has(queryPath)) {
useDistinct = true
} else {
visitedPaths.add(queryPath)
}
}
chainedMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
// When we have any joins, we need to count each individual ID only once.
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
const countResult = await chainMethods({
methods: chainedMethods,
query: db
query: (db as TransactionPg)
.select({
count: sql`COUNT(1) OVER()`,
count: useDistinct ? sql`COUNT(DISTINCT ${this.tables[tableName].id})` : count(),
})
.from(this.tables[tableName])
.where(where)
.groupBy(this.tables[tableName].id)
.limit(1),
.where(where),
})
return Number(countResult[0].count)

View File

@@ -1,4 +1,4 @@
import type { FlattenedBlock, FlattenedField } from 'payload'
import type { FlattenedField } from 'payload'
type Args = {
doc: Record<string, unknown>
@@ -51,10 +51,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
Object.entries(rowData).forEach(([locale, localeRows]) => {
if (Array.isArray(localeRows)) {
localeRows.forEach((row, i) => {
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
const matchedBlock = field.blocks.find(
(block) => typeof block !== 'string' && block.slug === row.blockType,
) as FlattenedBlock | undefined
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
if (matchedBlock) {
return traverseFields({
@@ -72,10 +69,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
if (Array.isArray(rowData)) {
rowData.forEach((row, i) => {
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
const matchedBlock = field.blocks.find(
(block) => typeof block !== 'string' && block.slug === row.blockType,
) as FlattenedBlock | undefined
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
if (matchedBlock) {
return traverseFields({

View File

@@ -42,11 +42,6 @@ export const traverseFields = (args: Args) => {
case 'blocks': {
return field.blocks.forEach((block) => {
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
if (typeof block === 'string') {
return
}
const newTableName = args.adapter.tableNameMap.get(
`${args.rootTableName}_blocks_${toSnakeCase(block.slug)}`,
)

View File

@@ -12,7 +12,6 @@ export function buildAndOrConditions({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,
@@ -25,7 +24,6 @@ export function buildAndOrConditions({
globalSlug?: string
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
selectLocale?: boolean
tableName: string
@@ -44,7 +42,6 @@ export function buildAndOrConditions({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,

View File

@@ -15,7 +15,6 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
sort?: Sort
tableName: string
@@ -30,7 +29,6 @@ export const buildOrderBy = ({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
sort,
tableName,
@@ -67,7 +65,6 @@ export const buildOrderBy = ({
fields,
joins,
locale,
parentIsLocalized,
pathSegments: sortProperty.replace(/__/g, '.').split('.'),
selectFields,
tableName,

View File

@@ -20,7 +20,6 @@ type BuildQueryArgs = {
fields: FlattenedField[]
joins?: BuildQueryJoinAliases
locale?: string
parentIsLocalized?: boolean
selectLocale?: boolean
sort?: Sort
tableName: string
@@ -42,7 +41,6 @@ const buildQuery = function buildQuery({
fields,
joins = [],
locale,
parentIsLocalized,
selectLocale,
sort,
tableName,
@@ -58,7 +56,6 @@ const buildQuery = function buildQuery({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
sort,
tableName,
@@ -73,7 +70,6 @@ const buildQuery = function buildQuery({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,

View File

@@ -1,11 +1,11 @@
import type { SQL } from 'drizzle-orm'
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
import type { FlattenedBlock, FlattenedField, NumberField, TextField } from 'payload'
import type { FlattenedField, NumberField, TextField } from 'payload'
import { and, eq, like, sql } from 'drizzle-orm'
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
import { APIError } from 'payload'
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
import { tabHasName } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import { validate as uuidValidate } from 'uuid'
@@ -46,7 +46,6 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
pathSegments: string[]
rootTableName?: string
selectFields: Record<string, GenericColumn>
@@ -76,7 +75,6 @@ export const getTableColumnFromPath = ({
fields,
joins,
locale: incomingLocale,
parentIsLocalized,
pathSegments: incomingSegments,
rootTableName: incomingRootTableName,
selectFields,
@@ -109,11 +107,9 @@ export const getTableColumnFromPath = ({
if (field) {
const pathSegments = [...incomingSegments]
const isFieldLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
// If next segment is a locale,
// we need to take it out and use it as the locale from this point on
if (isFieldLocalized && adapter.payload.config.localization) {
if ('localized' in field && field.localized && adapter.payload.config.localization) {
const matchedLocale = adapter.payload.config.localization.localeCodes.find(
(locale) => locale === pathSegments[1],
)
@@ -133,7 +129,7 @@ export const getTableColumnFromPath = ({
const arrayParentTable = aliasTable || adapter.tables[tableName]
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && isFieldLocalized && adapter.payload.config.localization) {
if (locale && field.localized && adapter.payload.config.localization) {
const conditions = [eq(arrayParentTable.id, adapter.tables[newTableName]._parentID)]
if (selectLocale) {
@@ -163,7 +159,6 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
@@ -181,12 +176,7 @@ export const getTableColumnFromPath = ({
// find the block config using the value
const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((blockType) => {
const block =
adapter.payload.blocks[blockType] ??
((field.blockReferences ?? field.blocks).find(
(block) => typeof block !== 'string' && block.slug === blockType,
) as FlattenedBlock | undefined)
const block = field.blocks.find((block) => block.slug === blockType)
newTableName = adapter.tableNameMap.get(
`${tableName}_blocks_${toSnakeCase(block.slug)}`,
)
@@ -211,46 +201,13 @@ export const getTableColumnFromPath = ({
}
}
const hasBlockField = (field.blockReferences ?? field.blocks).some((_block) => {
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
const hasBlockField = field.blocks.some((block) => {
newTableName = adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`)
constraintPath = `${constraintPath}${field.name}.%.`
let result: TableColumn
let result
const blockConstraints = []
const blockSelectFields = {}
let blockJoin: BuildQueryJoinAliases[0]
if (isFieldLocalized && adapter.payload.config.localization) {
const conditions = [
eq(
(aliasTable || adapter.tables[tableName]).id,
adapter.tables[newTableName]._parentID,
),
]
if (locale !== 'all') {
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
}
blockJoin = {
condition: and(...conditions),
table: adapter.tables[newTableName],
}
} else {
blockJoin = {
condition: eq(
(aliasTable || adapter.tables[tableName]).id,
adapter.tables[newTableName]._parentID,
),
table: adapter.tables[newTableName],
}
}
// Create a new reference for nested joins
const newJoins = [...joins]
try {
result = getTableColumnFromPath({
adapter,
@@ -258,9 +215,8 @@ export const getTableColumnFromPath = ({
constraintPath,
constraints: blockConstraints,
fields: block.flattenedFields,
joins: newJoins,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields: blockSelectFields,
@@ -277,14 +233,30 @@ export const getTableColumnFromPath = ({
blockTableColumn = result
constraints = constraints.concat(blockConstraints)
selectFields = { ...selectFields, ...blockSelectFields }
if (field.localized && adapter.payload.config.localization) {
const conditions = [
eq(
(aliasTable || adapter.tables[tableName]).id,
adapter.tables[newTableName]._parentID,
),
]
const previousLength = joins.length
joins.push(blockJoin)
// Append new joins AFTER the block join to prevent errors with missing FROM clause.
if (newJoins.length > previousLength) {
for (let i = previousLength; i < newJoins.length; i++) {
joins.push(newJoins[i])
if (locale !== 'all') {
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
}
joins.push({
condition: and(...conditions),
table: adapter.tables[newTableName],
})
} else {
joins.push({
condition: eq(
(aliasTable || adapter.tables[tableName]).id,
adapter.tables[newTableName]._parentID,
),
table: adapter.tables[newTableName],
})
}
return true
})
@@ -302,7 +274,7 @@ export const getTableColumnFromPath = ({
}
case 'group': {
if (locale && isFieldLocalized && adapter.payload.config.localization) {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}${adapter.localesSuffix}`
let condition = eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID)
@@ -327,7 +299,6 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
@@ -353,7 +324,7 @@ export const getTableColumnFromPath = ({
like(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && isFieldLocalized && adapter.payload.config.localization) {
if (locale && field.localized && adapter.payload.config.localization) {
const conditions = [...joinConstraints]
if (locale !== 'all') {
@@ -397,12 +368,12 @@ export const getTableColumnFromPath = ({
tableName: relationTableName,
})
if (selectLocale && isFieldLocalized && adapter.payload.config.localization) {
if (selectLocale && field.localized && adapter.payload.config.localization) {
selectFields._locale = aliasRelationshipTable.locale
}
// Join in the relationships table
if (locale && isFieldLocalized && adapter.payload.config.localization) {
if (locale && field.localized && adapter.payload.config.localization) {
const conditions = [
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
@@ -568,11 +539,9 @@ export const getTableColumnFromPath = ({
aliasTable: newAliasTable,
collectionPath: newCollectionPath,
constraints,
// relationshipFields are fields from a different collection => no parentIsLocalized
fields: relationshipFields,
joins,
locale,
parentIsLocalized: false,
pathSegments: pathSegments.slice(1),
rootTableName: newTableName,
selectFields,
@@ -591,7 +560,7 @@ export const getTableColumnFromPath = ({
)
const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName })
if (isFieldLocalized && adapter.payload.config.localization) {
if (field.localized && adapter.payload.config.localization) {
const { newAliasTable: aliasLocaleTable } = getTableAlias({
adapter,
tableName: `${rootTableName}${adapter.localesSuffix}`,
@@ -638,7 +607,6 @@ export const getTableColumnFromPath = ({
fields: adapter.payload.collections[field.relationTo].config.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
selectFields,
tableName: newTableName,
@@ -654,7 +622,7 @@ export const getTableColumnFromPath = ({
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
if (locale && isFieldLocalized && adapter.payload.config.localization) {
if (locale && field.localized && adapter.payload.config.localization) {
const conditions = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName]._locale, locale),
@@ -699,7 +667,6 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
@@ -719,7 +686,6 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
@@ -738,7 +704,7 @@ export const getTableColumnFromPath = ({
let newTable = adapter.tables[newTableName]
if (isFieldLocalized && adapter.payload.config.localization) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
const parentTable = aliasTable || adapter.tables[tableName]

View File

@@ -20,7 +20,6 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
selectLocale?: boolean
tableName: string
@@ -33,7 +32,6 @@ export function parseParams({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,
@@ -60,7 +58,6 @@ export function parseParams({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,
@@ -95,7 +92,6 @@ export function parseParams({
fields,
joins,
locale,
parentIsLocalized,
pathSegments: relationOrPath.replace(/__/g, '.').split('.'),
selectFields,
selectLocale,

View File

@@ -37,7 +37,6 @@ type Args = {
disableRelsTableUnique?: boolean
disableUnique: boolean
fields: FlattenedField[]
parentIsLocalized: boolean
rootRelationships?: Set<string>
rootRelationsToBuild?: RelationMap
rootTableIDColType?: IDType
@@ -72,7 +71,6 @@ export const buildTable = ({
disableRelsTableUnique = false,
disableUnique = false,
fields,
parentIsLocalized,
rootRelationships,
rootRelationsToBuild,
rootTableIDColType,
@@ -126,7 +124,6 @@ export const buildTable = ({
localesColumns,
localesIndexes,
newTableName: tableName,
parentIsLocalized,
parentTableName: tableName,
relationships,
relationsToBuild,

View File

@@ -55,7 +55,6 @@ export const buildRawSchema = ({
disableNotNull: !!collection?.versions?.drafts,
disableUnique: false,
fields: collection.flattenedFields,
parentIsLocalized: false,
setColumnID,
tableName,
timestamps: collection.timestamps,
@@ -73,7 +72,6 @@ export const buildRawSchema = ({
disableNotNull: !!collection.versions?.drafts,
disableUnique: true,
fields: versionFields,
parentIsLocalized: false,
setColumnID,
tableName: versionsTableName,
timestamps: true,
@@ -93,7 +91,6 @@ export const buildRawSchema = ({
disableNotNull: !!global?.versions?.drafts,
disableUnique: false,
fields: global.flattenedFields,
parentIsLocalized: false,
setColumnID,
tableName,
timestamps: false,
@@ -115,7 +112,6 @@ export const buildRawSchema = ({
disableNotNull: !!global.versions?.drafts,
disableUnique: true,
fields: versionFields,
parentIsLocalized: false,
setColumnID,
tableName: versionsTableName,
timestamps: true,

View File

@@ -1,12 +1,7 @@
import type { FlattenedField } from 'payload'
import { InvalidConfiguration } from 'payload'
import {
fieldAffectsData,
fieldIsVirtual,
fieldShouldBeLocalized,
optionIsObject,
} from 'payload/shared'
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type {
@@ -42,7 +37,6 @@ type Args = {
localesColumns: Record<string, RawColumn>
localesIndexes: Record<string, RawIndex>
newTableName: string
parentIsLocalized: boolean
parentTableName: string
relationships: Set<string>
relationsToBuild: RelationMap
@@ -82,7 +76,6 @@ export const traverseFields = ({
localesColumns,
localesIndexes,
newTableName,
parentIsLocalized,
parentTableName,
relationships,
relationsToBuild,
@@ -126,13 +119,11 @@ export const traverseFields = ({
)}`
const fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}`
const isFieldLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
// If field is localized,
// add the column to the locale table instead of main table
if (
adapter.payload.config.localization &&
(isFieldLocalized || forceLocalized) &&
(field.localized || forceLocalized) &&
field.type !== 'array' &&
field.type !== 'blocks' &&
(('hasMany' in field && field.hasMany !== true) || !('hasMany' in field))
@@ -161,7 +152,7 @@ export const traverseFields = ({
targetIndexes[indexName] = {
name: indexName,
on: isFieldLocalized ? [fieldName, '_locale'] : fieldName,
on: field.localized ? [fieldName, '_locale'] : fieldName,
unique,
}
}
@@ -218,7 +209,7 @@ export const traverseFields = ({
}
const isLocalized =
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
@@ -252,7 +243,6 @@ export const traverseFields = ({
disableRelsTableUnique: true,
disableUnique,
fields: disableUnique ? idToUUID(field.flattenedFields) : field.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
rootRelationships: relationships,
rootRelationsToBuild,
rootTableIDColType,
@@ -309,12 +299,7 @@ export const traverseFields = ({
},
}
if (
hasLocalesTable({
fields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
})
) {
if (hasLocalesTable(field.fields)) {
arrayRelations._locales = {
type: 'many',
relationName: '_locales',
@@ -358,9 +343,7 @@ export const traverseFields = ({
case 'blocks': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
;(field.blockReferences ?? field.blocks).forEach((_block) => {
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
field.blocks.forEach((block) => {
const blockTableName = createTableName({
adapter,
config: block,
@@ -418,7 +401,7 @@ export const traverseFields = ({
}
const isLocalized =
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
@@ -452,7 +435,6 @@ export const traverseFields = ({
disableRelsTableUnique: true,
disableUnique,
fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
rootRelationships: relationships,
rootRelationsToBuild,
rootTableIDColType,
@@ -503,12 +485,7 @@ export const traverseFields = ({
},
}
if (
hasLocalesTable({
fields: block.fields,
parentIsLocalized: parentIsLocalized || field.localized,
})
) {
if (hasLocalesTable(block.fields)) {
blockRelations._locales = {
type: 'many',
relationName: '_locales',
@@ -550,7 +527,6 @@ export const traverseFields = ({
validateExistingBlockIsIdentical({
block,
localized: field.localized,
parentIsLocalized: parentIsLocalized || field.localized,
rootTableName,
table: adapter.rawTables[blockTableName],
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
@@ -627,12 +603,11 @@ export const traverseFields = ({
disableUnique,
fieldPrefix: `${fieldName}.`,
fields: field.flattenedFields,
forceLocalized: isFieldLocalized,
forceLocalized: field.localized,
indexes,
localesColumns,
localesIndexes,
newTableName: `${parentTableName}_${columnName}`,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName,
relationships,
relationsToBuild,
@@ -642,7 +617,7 @@ export const traverseFields = ({
setColumnID,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || isFieldLocalized,
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
})
if (groupHasLocalizedField) {
@@ -682,7 +657,7 @@ export const traverseFields = ({
case 'number': {
if (field.hasMany) {
const isLocalized =
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
@@ -807,7 +782,7 @@ export const traverseFields = ({
}
const isLocalized =
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
@@ -840,7 +815,6 @@ export const traverseFields = ({
disableNotNull,
disableUnique,
fields: [],
parentIsLocalized: parentIsLocalized || field.localized,
rootTableName,
setColumnID,
tableName: selectTableName,
@@ -928,7 +902,7 @@ export const traverseFields = ({
// add relationship to table
relationsToBuild.set(fieldName, {
type: 'one',
localized: adapter.payload.config.localization && (isFieldLocalized || forceLocalized),
localized: adapter.payload.config.localization && (field.localized || forceLocalized),
target: tableName,
})
@@ -940,7 +914,7 @@ export const traverseFields = ({
}
if (
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
) {
hasLocalizedRelationshipField = true
@@ -951,7 +925,7 @@ export const traverseFields = ({
case 'text': {
if (field.hasMany) {
const isLocalized =
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized

View File

@@ -14,7 +14,6 @@ type TransformArgs = {
fields: FlattenedField[]
joinQuery?: JoinQuery
locale?: string
parentIsLocalized?: boolean
}
// This is the entry point to transform Drizzle output data
@@ -25,7 +24,6 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
data,
fields,
joinQuery,
parentIsLocalized,
}: TransformArgs): T => {
let relationships: Record<string, Record<string, unknown>[]> = {}
let texts: Record<string, Record<string, unknown>[]> = {}
@@ -61,7 +59,6 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
fields,
joinQuery,
numbers,
parentIsLocalized,
path: '',
relationships,
table: data,

View File

@@ -1,6 +1,6 @@
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
import type { FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
import type { DrizzleAdapter } from '../../types.js'
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
@@ -46,7 +46,6 @@ type TraverseFieldsArgs = {
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path
*/
numbers: Record<string, Record<string, unknown>[]>
parentIsLocalized: boolean
/**
* The current field path (in dot notation), used to merge in relationships
*/
@@ -81,7 +80,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
fields,
joinQuery,
numbers,
parentIsLocalized,
path,
relationships,
table,
@@ -107,11 +105,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions.push(() => delete table[fieldName])
}
const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
if (field.type === 'array') {
if (Array.isArray(fieldData)) {
if (isLocalized) {
if (field.localized) {
result[field.name] = fieldData.reduce((arrayResult, row) => {
if (typeof row._locale === 'string') {
if (!arrayResult[row._locale]) {
@@ -134,7 +130,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: field.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships,
table: row,
@@ -180,7 +175,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: field.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}.${i}`,
relationships,
table: row,
@@ -203,7 +197,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
const blocksByPath = blocks[blockFieldPath]
if (Array.isArray(blocksByPath)) {
if (isLocalized) {
if (field.localized) {
result[field.name] = {}
blocksByPath.forEach((row) => {
@@ -222,11 +216,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
Object.entries(result[field.name]).forEach(([locale, localizedBlocks]) => {
result[field.name][locale] = localizedBlocks.map((row) => {
const block =
adapter.payload.blocks[row.blockType] ??
((field.blockReferences ?? field.blocks).find(
(block) => typeof block !== 'string' && block.slug === row.blockType,
) as FlattenedBlock | undefined)
const block = field.blocks.find(({ slug }) => slug === row.blockType)
if (block) {
const blockResult = traverseFields<T>({
@@ -238,7 +228,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: block.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${blockFieldPath}.${row._order - 1}`,
relationships,
table: row,
@@ -276,16 +265,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
row.id = row._uuid
delete row._uuid
}
if (typeof row.blockType !== 'string') {
return acc
}
const block =
adapter.payload.blocks[row.blockType] ??
((field.blockReferences ?? field.blocks).find(
(block) => typeof block !== 'string' && block.slug === row.blockType,
) as FlattenedBlock | undefined)
const block = field.blocks.find(({ slug }) => slug === row.blockType)
if (block) {
if (
@@ -310,7 +290,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: block.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
@@ -336,7 +315,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (field.type === 'relationship' || field.type === 'upload') {
if (typeof field.relationTo === 'string' && !('hasMany' in field && field.hasMany)) {
if (
isLocalized &&
field.localized &&
config.localization &&
config.localization.locales &&
Array.isArray(table?._locales)
@@ -352,7 +331,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (!relationPathMatch) {
if ('hasMany' in field && field.hasMany) {
if (isLocalized && config.localization && config.localization.locales) {
if (field.localized && config.localization && config.localization.locales) {
result[field.name] = {
[config.localization.defaultLocale]: [],
}
@@ -364,7 +343,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (isLocalized) {
if (field.localized) {
result[field.name] = {}
const relationsByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -410,7 +389,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
| { docs: unknown[]; hasNextPage: boolean }
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
if (Array.isArray(fieldData)) {
if (isLocalized && adapter.payload.config.localization) {
if (field.localized && adapter.payload.config.localization) {
fieldResult = fieldData.reduce(
(joinResult, row) => {
if (typeof row.locale === 'string') {
@@ -436,14 +415,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
} else {
const hasNextPage = limit !== 0 && fieldData.length > limit
fieldResult = {
docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(
({ id, relationTo }) => {
if (relationTo) {
return { relationTo, value: id }
}
return { id }
},
),
docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(({ id }) => ({
id,
})),
hasNextPage,
}
}
@@ -459,7 +433,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (isLocalized) {
if (field.localized) {
result[field.name] = {}
const textsByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -498,7 +472,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (isLocalized) {
if (field.localized) {
result[field.name] = {}
const numbersByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -533,7 +507,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (field.type === 'select' && field.hasMany) {
if (Array.isArray(fieldData)) {
if (isLocalized) {
if (field.localized) {
result[field.name] = fieldData.reduce((selectResult, row) => {
if (typeof row.locale === 'string') {
if (!selectResult[row.locale]) {
@@ -555,7 +529,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (isLocalized && Array.isArray(table._locales)) {
if (field.localized && Array.isArray(table._locales)) {
if (!table._locales.length && adapter.payload.config.localization) {
adapter.payload.config.localization.localeCodes.forEach((_locale) =>
(table._locales as unknown[]).push({ _locale }),
@@ -594,9 +568,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_`
const groupData = {}
const locale = table._locale as string
const refKey = isLocalized && locale ? locale : field.name
const refKey = field.localized && locale ? locale : field.name
if (isLocalized && locale) {
if (field.localized && locale) {
delete table._locale
}
ref[refKey] = traverseFields<Record<string, unknown>>({
@@ -608,7 +582,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: groupFieldPrefix,
fields: field.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}`,
relationships,
table,

View File

@@ -1,7 +1,5 @@
import type { FlattenedArrayField } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import type { DrizzleAdapter } from '../../types.js'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
@@ -20,7 +18,6 @@ type Args = {
field: FlattenedArrayField
locale?: string
numbers: Record<string, unknown>[]
parentIsLocalized: boolean
path: string
relationships: Record<string, unknown>[]
relationshipsToDelete: RelationshipToDelete[]
@@ -45,7 +42,6 @@ export const transformArray = ({
field,
locale,
numbers,
parentIsLocalized,
path,
relationships,
relationshipsToDelete,
@@ -83,7 +79,7 @@ export const transformArray = ({
}
}
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && locale) {
if (field.localized) {
newRow.row._locale = locale
}
@@ -104,7 +100,6 @@ export const transformArray = ({
insideArrayOrBlock: true,
locales: newRow.locales,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName: arrayTableName,
path: `${path || ''}${field.name}.${i}.`,
relationships,

View File

@@ -1,6 +1,5 @@
import type { FlattenedBlock, FlattenedBlocksField } from 'payload'
import type { FlattenedBlocksField } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
@@ -19,7 +18,6 @@ type Args = {
field: FlattenedBlocksField
locale?: string
numbers: Record<string, unknown>[]
parentIsLocalized: boolean
path: string
relationships: Record<string, unknown>[]
relationshipsToDelete: RelationshipToDelete[]
@@ -42,7 +40,6 @@ export const transformBlocks = ({
field,
locale,
numbers,
parentIsLocalized,
path,
relationships,
relationshipsToDelete,
@@ -54,13 +51,7 @@ export const transformBlocks = ({
if (typeof blockRow.blockType !== 'string') {
return
}
const matchedBlock =
adapter.payload.blocks[blockRow.blockType] ??
((field.blockReferences ?? field.blocks).find(
(block) => typeof block !== 'string' && block.slug === blockRow.blockType,
) as FlattenedBlock | undefined)
const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType)
if (!matchedBlock) {
return
}
@@ -79,7 +70,7 @@ export const transformBlocks = ({
},
}
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && locale) {
if (field.localized && locale) {
newRow.row._locale = locale
}
if (withinArrayOrBlockLocale) {
@@ -113,7 +104,6 @@ export const transformBlocks = ({
insideArrayOrBlock: true,
locales: newRow.locales,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName: blockTableName,
path: `${path || ''}${field.name}.${i}.`,
relationships,

View File

@@ -9,7 +9,6 @@ type Args = {
adapter: DrizzleAdapter
data: Record<string, unknown>
fields: FlattenedField[]
parentIsLocalized?: boolean
path?: string
tableName: string
}
@@ -18,7 +17,6 @@ export const transformForWrite = ({
adapter,
data,
fields,
parentIsLocalized,
path = '',
tableName,
}: Args): RowToInsert => {
@@ -50,7 +48,6 @@ export const transformForWrite = ({
fields,
locales: rowToInsert.locales,
numbers: rowToInsert.numbers,
parentIsLocalized,
parentTableName: tableName,
path,
relationships: rowToInsert.relationships,

View File

@@ -1,7 +1,7 @@
import type { FlattenedField } from 'payload'
import { sql } from 'drizzle-orm'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import { fieldIsVirtual } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
@@ -50,7 +50,6 @@ type Args = {
[locale: string]: Record<string, unknown>
}
numbers: Record<string, unknown>[]
parentIsLocalized: boolean
/**
* This is the name of the parent table
*/
@@ -85,7 +84,6 @@ export const traverseFields = ({
insideArrayOrBlock = false,
locales,
numbers,
parentIsLocalized,
parentTableName,
path,
relationships,
@@ -112,8 +110,6 @@ export const traverseFields = ({
fieldName = `${fieldPrefix || ''}${field.name}`
fieldData = data[field.name]
const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
if (field.type === 'array') {
const arrayTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
@@ -121,7 +117,7 @@ export const traverseFields = ({
arrays[arrayTableName] = []
}
if (isLocalized) {
if (field.localized) {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -135,7 +131,6 @@ export const traverseFields = ({
field,
locale: localeKey,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
@@ -158,7 +153,6 @@ export const traverseFields = ({
data: data[field.name],
field,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
@@ -174,11 +168,11 @@ export const traverseFields = ({
}
if (field.type === 'blocks') {
;(field.blockReferences ?? field.blocks).forEach((block) => {
blocksToDelete.add(toSnakeCase(typeof block === 'string' ? block : block.slug))
field.blocks.forEach(({ slug }) => {
blocksToDelete.add(toSnakeCase(slug))
})
if (isLocalized) {
if (field.localized) {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -191,7 +185,6 @@ export const traverseFields = ({
field,
locale: localeKey,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
@@ -211,7 +204,6 @@ export const traverseFields = ({
data: fieldData,
field,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
@@ -226,7 +218,7 @@ export const traverseFields = ({
if (field.type === 'group' || field.type === 'tab') {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
if (isLocalized) {
if (field.localized) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
// preserve array ID if there is
localeData._uuid = data.id || data._uuid
@@ -246,7 +238,6 @@ export const traverseFields = ({
insideArrayOrBlock,
locales,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName,
path: `${path || ''}${field.name}.`,
relationships,
@@ -276,7 +267,6 @@ export const traverseFields = ({
insideArrayOrBlock,
locales,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName,
path: `${path || ''}${field.name}.`,
relationships,
@@ -296,7 +286,7 @@ export const traverseFields = ({
const relationshipPath = `${path || ''}${field.name}`
if (
isLocalized &&
field.localized &&
(Array.isArray(field.relationTo) || ('hasMany' in field && field.hasMany))
) {
if (typeof fieldData === 'object') {
@@ -339,14 +329,14 @@ export const traverseFields = ({
return
} else {
if (
!isLocalized &&
!field.localized &&
fieldData &&
typeof fieldData === 'object' &&
'id' in fieldData &&
fieldData?.id
) {
fieldData = fieldData.id
} else if (isLocalized) {
} else if (field.localized) {
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (typeof localeData === 'object') {
@@ -365,7 +355,7 @@ export const traverseFields = ({
if (field.type === 'text' && field.hasMany) {
const textPath = `${path || ''}${field.name}`
if (isLocalized) {
if (field.localized) {
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -397,7 +387,7 @@ export const traverseFields = ({
if (field.type === 'number' && field.hasMany) {
const numberPath = `${path || ''}${field.name}`
if (isLocalized) {
if (field.localized) {
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -432,7 +422,7 @@ export const traverseFields = ({
selects[selectTableName] = []
}
if (isLocalized) {
if (field.localized) {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -461,7 +451,7 @@ export const traverseFields = ({
const valuesToTransform: { localeKey?: string; ref: unknown; value: unknown }[] = []
if (isLocalized) {
if (field.localized) {
if (typeof fieldData === 'object' && fieldData !== null) {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (!locales[localeKey]) {

View File

@@ -1,38 +1,21 @@
import type { Field } from 'payload'
import { fieldAffectsData, fieldHasSubFields, fieldShouldBeLocalized } from 'payload/shared'
import { fieldAffectsData, fieldHasSubFields } from 'payload/shared'
export const hasLocalesTable = ({
fields,
parentIsLocalized,
}: {
fields: Field[]
/**
* @todo make required in v4.0. Usually you'd wanna pass this in
*/
parentIsLocalized?: boolean
}): boolean => {
export const hasLocalesTable = (fields: Field[]): boolean => {
return fields.some((field) => {
// arrays always get a separate table
if (field.type === 'array') {
return false
}
if (fieldAffectsData(field) && fieldShouldBeLocalized({ field, parentIsLocalized })) {
if (fieldAffectsData(field) && field.localized) {
return true
}
if (fieldHasSubFields(field)) {
return hasLocalesTable({
fields: field.fields,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
})
return hasLocalesTable(field.fields)
}
if (field.type === 'tabs') {
return field.tabs.some((tab) =>
hasLocalesTable({
fields: tab.fields,
parentIsLocalized: parentIsLocalized || tab.localized,
}),
)
return field.tabs.some((tab) => hasLocalesTable(tab.fields))
}
return false
})

View File

@@ -1,33 +1,22 @@
import type { Block, Field } from 'payload'
import { InvalidConfiguration } from 'payload'
import {
fieldAffectsData,
fieldHasSubFields,
fieldShouldBeLocalized,
tabHasName,
} from 'payload/shared'
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/shared'
import type { RawTable } from '../types.js'
type Args = {
block: Block
localized: boolean
/**
* @todo make required in v4.0. Usually you'd wanna pass this in
*/
parentIsLocalized?: boolean
rootTableName: string
table: RawTable
tableLocales?: RawTable
}
const getFlattenedFieldNames = (args: {
fields: Field[]
parentIsLocalized: boolean
prefix?: string
}): { localized?: boolean; name: string }[] => {
const { fields, parentIsLocalized, prefix = '' } = args
const getFlattenedFieldNames = (
fields: Field[],
prefix: string = '',
): { localized?: boolean; name: string }[] => {
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
@@ -40,14 +29,7 @@ const getFlattenedFieldNames = (args: {
if (fieldHasSubFields(field)) {
fieldPrefix = 'name' in field ? `${prefix}${field.name}_` : prefix
return [
...fieldsToUse,
...getFlattenedFieldNames({
fields: field.fields,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
prefix: fieldPrefix,
}),
]
return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)]
}
if (field.type === 'tabs') {
@@ -59,11 +41,7 @@ const getFlattenedFieldNames = (args: {
...tabFields,
...(tabHasName(tab)
? [{ ...tab, type: 'tab' }]
: getFlattenedFieldNames({
fields: tab.fields,
parentIsLocalized: parentIsLocalized || tab.localized,
prefix: fieldPrefix,
})),
: getFlattenedFieldNames(tab.fields, fieldPrefix)),
]
}, []),
]
@@ -74,7 +52,7 @@ const getFlattenedFieldNames = (args: {
...fieldsToUse,
{
name: `${fieldPrefix}${field.name}`,
localized: fieldShouldBeLocalized({ field, parentIsLocalized }),
localized: field.localized,
},
]
}
@@ -86,15 +64,11 @@ const getFlattenedFieldNames = (args: {
export const validateExistingBlockIsIdentical = ({
block,
localized,
parentIsLocalized,
rootTableName,
table,
tableLocales,
}: Args): void => {
const fieldNames = getFlattenedFieldNames({
fields: block.fields,
parentIsLocalized: parentIsLocalized || localized,
})
const fieldNames = getFlattenedFieldNames(block.fields)
const missingField =
// ensure every field from the config is in the matching table

View File

@@ -75,7 +75,6 @@ type BuildMutationInputTypeArgs = {
forceNullable?: boolean
graphqlResult: GraphQLInfo
name: string
parentIsLocalized: boolean
parentName: string
}
@@ -85,7 +84,6 @@ export function buildMutationInputType({
fields,
forceNullable = false,
graphqlResult,
parentIsLocalized,
parentName,
}: BuildMutationInputTypeArgs): GraphQLInputObjectType | null {
const fieldToSchemaMap = {
@@ -96,7 +94,6 @@ export function buildMutationInputType({
config,
fields: field.fields,
graphqlResult,
parentIsLocalized: parentIsLocalized || field.localized,
parentName: fullName,
})
@@ -104,7 +101,7 @@ export function buildMutationInputType({
return inputObjectTypeConfig
}
type = new GraphQLList(withNullableType({ type, field, forceNullable, parentIsLocalized }))
type = new GraphQLList(withNullableType(field, type, forceNullable))
return {
...inputObjectTypeConfig,
[field.name]: { type },
@@ -120,9 +117,7 @@ export function buildMutationInputType({
}),
code: (inputObjectTypeConfig: InputObjectTypeConfig, field: CodeField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
collapsible: (inputObjectTypeConfig: InputObjectTypeConfig, field: CollapsibleField) =>
field.fields.reduce((acc, subField: CollapsibleField) => {
@@ -134,15 +129,11 @@ export function buildMutationInputType({
}, inputObjectTypeConfig),
date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
email: (inputObjectTypeConfig: InputObjectTypeConfig, field: EmailField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
group: (inputObjectTypeConfig: InputObjectTypeConfig, field: GroupField) => {
const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field)
@@ -152,7 +143,6 @@ export function buildMutationInputType({
config,
fields: field.fields,
graphqlResult,
parentIsLocalized: parentIsLocalized || field.localized,
parentName: fullName,
})
@@ -170,40 +160,28 @@ export function buildMutationInputType({
},
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) },
}),
number: (inputObjectTypeConfig: InputObjectTypeConfig, field: NumberField) => {
const type = field.name === 'id' ? GraphQLInt : GraphQLFloat
return {
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({
type: field.hasMany === true ? new GraphQLList(type) : type,
type: withNullableType(
field,
field.hasMany === true ? new GraphQLList(type) : type,
forceNullable,
parentIsLocalized,
}),
),
},
}
},
point: (inputObjectTypeConfig: InputObjectTypeConfig, field: PointField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({
type: new GraphQLList(GraphQLFloat),
field,
forceNullable,
parentIsLocalized,
}),
},
[field.name]: { type: withNullableType(field, new GraphQLList(GraphQLFloat), forceNullable) },
}),
radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
relationship: (inputObjectTypeConfig: InputObjectTypeConfig, field: RelationshipField) => {
const { relationTo } = field
@@ -252,9 +230,7 @@ export function buildMutationInputType({
},
richText: (inputObjectTypeConfig: InputObjectTypeConfig, field: RichTextField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) },
}),
row: (inputObjectTypeConfig: InputObjectTypeConfig, field: RowField) =>
field.fields.reduce((acc, subField: Field) => {
@@ -288,7 +264,7 @@ export function buildMutationInputType({
})
type = field.hasMany ? new GraphQLList(type) : type
type = withNullableType({ type, field, forceNullable, parentIsLocalized })
type = withNullableType(field, type, forceNullable)
return {
...inputObjectTypeConfig,
@@ -305,7 +281,6 @@ export function buildMutationInputType({
config,
fields: tab.fields,
graphqlResult,
parentIsLocalized: parentIsLocalized || tab.localized,
parentName: fullName,
})
@@ -337,19 +312,16 @@ export function buildMutationInputType({
text: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
type: withNullableType(
field,
field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
forceNullable,
parentIsLocalized,
}),
),
},
}),
textarea: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextareaField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => {
const { relationTo } = field

View File

@@ -62,7 +62,6 @@ type Args = {
forceNullable?: boolean
graphqlResult: GraphQLInfo
name: string
parentIsLocalized?: boolean
parentName: string
}
@@ -73,7 +72,6 @@ export function buildObjectType({
fields,
forceNullable,
graphqlResult,
parentIsLocalized,
parentName,
}: Args): GraphQLObjectType {
const fieldToSchemaMap = {
@@ -86,9 +84,8 @@ export function buildObjectType({
name: interfaceName,
config,
fields: field.fields,
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
forceNullable: isFieldNullable(field, forceNullable),
graphqlResult,
parentIsLocalized: field.localized || parentIsLocalized,
parentName: interfaceName,
})
@@ -107,19 +104,12 @@ export function buildObjectType({
return {
...objectTypeConfig,
[field.name]: { type: withNullableType({ type: arrayType, field, parentIsLocalized }) },
[field.name]: { type: withNullableType(field, arrayType) },
}
},
blocks: (objectTypeConfig: ObjectTypeConfig, field: BlocksField) => {
const blockTypes: GraphQLObjectType<any, any>[] = (
field.blockReferences ?? field.blocks
).reduce((acc, _block) => {
const blockSlug = typeof _block === 'string' ? _block : _block.slug
if (!graphqlResult.types.blockTypes[blockSlug]) {
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
const block =
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
const blockTypes: GraphQLObjectType<any, any>[] = field.blocks.reduce((acc, block) => {
if (!graphqlResult.types.blockTypes[block.slug]) {
const interfaceName =
block?.interfaceName || block?.graphQL?.singularName || toWords(block.slug, true)
@@ -135,7 +125,6 @@ export function buildObjectType({
],
forceNullable,
graphqlResult,
parentIsLocalized,
parentName: interfaceName,
})
@@ -144,8 +133,8 @@ export function buildObjectType({
}
}
if (graphqlResult.types.blockTypes[blockSlug]) {
acc.push(graphqlResult.types.blockTypes[blockSlug])
if (graphqlResult.types.blockTypes[block.slug]) {
acc.push(graphqlResult.types.blockTypes[block.slug])
}
return acc
@@ -169,20 +158,16 @@ export function buildObjectType({
return {
...objectTypeConfig,
[field.name]: { type: withNullableType({ type, field, parentIsLocalized }) },
[field.name]: { type: withNullableType(field, type) },
}
},
checkbox: (objectTypeConfig: ObjectTypeConfig, field: CheckboxField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLBoolean, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLBoolean, forceNullable) },
}),
code: (objectTypeConfig: ObjectTypeConfig, field: CodeField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
collapsible: (objectTypeConfig: ObjectTypeConfig, field: CollapsibleField) =>
field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField) => {
@@ -194,20 +179,11 @@ export function buildObjectType({
}, objectTypeConfig),
date: (objectTypeConfig: ObjectTypeConfig, field: DateField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({ type: DateTimeResolver, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, DateTimeResolver, forceNullable) },
}),
email: (objectTypeConfig: ObjectTypeConfig, field: EmailField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({
type: EmailAddressResolver,
field,
forceNullable,
parentIsLocalized,
}),
},
[field.name]: { type: withNullableType(field, EmailAddressResolver, forceNullable) },
}),
group: (objectTypeConfig: ObjectTypeConfig, field: GroupField) => {
const interfaceName =
@@ -218,9 +194,8 @@ export function buildObjectType({
name: interfaceName,
config,
fields: field.fields,
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
forceNullable: isFieldNullable(field, forceNullable),
graphqlResult,
parentIsLocalized: field.localized || parentIsLocalized,
parentName: interfaceName,
})
@@ -254,9 +229,7 @@ export function buildObjectType({
name: joinName,
fields: {
docs: {
type: Array.isArray(field.collection)
? GraphQLJSON
: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
type: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
},
hasNextPage: { type: GraphQLBoolean },
},
@@ -265,16 +238,11 @@ export function buildObjectType({
limit: {
type: GraphQLInt,
},
page: {
type: GraphQLInt,
},
sort: {
type: GraphQLString,
},
where: {
type: Array.isArray(field.collection)
? GraphQLJSON
: graphqlResult.collections[field.collection].graphQL.whereInputType,
type: graphqlResult.collections[field.collection].graphQL.whereInputType,
},
},
extensions: {
@@ -283,17 +251,13 @@ export function buildObjectType({
},
async resolve(parent, args, context: Context) {
const { collection } = field
const { limit, page, sort, where } = args
const { limit, sort, where } = args
const { req } = context
const fullWhere = combineQueries(where, {
[field.on]: { equals: parent._id ?? parent.id },
})
if (Array.isArray(collection)) {
throw new Error('GraphQL with array of join.field.collection is not implemented')
}
return await req.payload.find({
collection,
depth: 0,
@@ -301,7 +265,6 @@ export function buildObjectType({
limit,
locale: req.locale,
overrideAccess: false,
page,
req,
sort,
where: fullWhere,
@@ -316,47 +279,42 @@ export function buildObjectType({
},
json: (objectTypeConfig: ObjectTypeConfig, field: JSONField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) },
}),
number: (objectTypeConfig: ObjectTypeConfig, field: NumberField) => {
const type = field?.name === 'id' ? GraphQLInt : GraphQLFloat
return {
...objectTypeConfig,
[field.name]: {
type: withNullableType({
type: field?.hasMany === true ? new GraphQLList(type) : type,
type: withNullableType(
field,
field?.hasMany === true ? new GraphQLList(type) : type,
forceNullable,
parentIsLocalized,
}),
),
},
}
},
point: (objectTypeConfig: ObjectTypeConfig, field: PointField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
type: withNullableType(
field,
new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
forceNullable,
parentIsLocalized,
}),
),
},
}),
radio: (objectTypeConfig: ObjectTypeConfig, field: RadioField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({
type: new GraphQLEnumType({
type: withNullableType(
field,
new GraphQLEnumType({
name: combineParentName(parentName, field.name),
values: formatOptions(field),
}),
field,
forceNullable,
parentIsLocalized,
}),
),
},
}),
relationship: (objectTypeConfig: ObjectTypeConfig, field: RelationshipField) => {
@@ -455,12 +413,11 @@ export function buildObjectType({
}
const relationship = {
type: withNullableType({
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
type: withNullableType(
field,
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
forceNullable,
parentIsLocalized,
}),
),
args: relationshipArgs,
extensions: {
complexity:
@@ -586,7 +543,7 @@ export function buildObjectType({
richText: (objectTypeConfig: ObjectTypeConfig, field: RichTextField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
type: withNullableType(field, GraphQLJSON, forceNullable),
args: {
depth: {
type: GraphQLInt,
@@ -627,7 +584,6 @@ export function buildObjectType({
findMany: false,
flattenLocales: false,
overrideAccess: false,
parentIsLocalized,
populationPromises,
req: context.req,
showHiddenFields: false,
@@ -658,7 +614,7 @@ export function buildObjectType({
})
type = field.hasMany ? new GraphQLList(new GraphQLNonNull(type)) : type
type = withNullableType({ type, field, forceNullable, parentIsLocalized })
type = withNullableType(field, type, forceNullable)
return {
...objectTypeConfig,
@@ -678,7 +634,6 @@ export function buildObjectType({
fields: tab.fields,
forceNullable,
graphqlResult,
parentIsLocalized: tab.localized || parentIsLocalized,
parentName: interfaceName,
})
@@ -719,19 +674,16 @@ export function buildObjectType({
text: (objectTypeConfig: ObjectTypeConfig, field: TextField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
type: withNullableType(
field,
field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
forceNullable,
parentIsLocalized,
}),
),
},
}),
textarea: (objectTypeConfig: ObjectTypeConfig, field: TextareaField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
const { relationTo } = field
@@ -816,12 +768,11 @@ export function buildObjectType({
}
const relationship = {
type: withNullableType({
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
type: withNullableType(
field,
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
forceNullable,
parentIsLocalized,
}),
),
args: relationshipArgs,
extensions: {
complexity:

View File

@@ -150,7 +150,6 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
config,
fields: mutationInputFields,
graphqlResult,
parentIsLocalized: false,
parentName: singularName,
})
if (createMutationInputType) {
@@ -165,7 +164,6 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
),
forceNullable: true,
graphqlResult,
parentIsLocalized: false,
parentName: `${singularName}Update`,
})
if (updateMutationInputType) {

View File

@@ -45,7 +45,6 @@ export function initGlobals({ config, graphqlResult }: InitGlobalsGraphQLArgs):
config,
fields,
graphqlResult,
parentIsLocalized: false,
parentName: formattedName,
})
graphqlResult.globals.graphQL[slug] = {

View File

@@ -2,23 +2,15 @@ import type { FieldAffectingData } from 'payload'
import { fieldAffectsData } from 'payload/shared'
export const isFieldNullable = ({
field,
forceNullable,
parentIsLocalized,
}: {
field: FieldAffectingData
forceNullable: boolean
parentIsLocalized: boolean
}): boolean => {
export const isFieldNullable = (field: FieldAffectingData, force: boolean): boolean => {
const hasReadAccessControl = field.access && field.access.read
const condition = field.admin && field.admin.condition
return !(
forceNullable &&
force &&
fieldAffectsData(field) &&
'required' in field &&
field.required &&
(!field.localized || parentIsLocalized) &&
!field.localized &&
!condition &&
!hasReadAccessControl
)

Some files were not shown because too many files have changed in this diff Show More