Compare commits
88 Commits
blocks-pg-
...
fix/checkD
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
764c42eb80 | ||
|
|
7626ff4635 | ||
|
|
9595e74153 | ||
|
|
58243a20e2 | ||
|
|
a62d86fd15 | ||
|
|
bd8ced1b60 | ||
|
|
0a664f9bac | ||
|
|
132852290a | ||
|
|
7f5aaad6a5 | ||
|
|
38c1c113ca | ||
|
|
7922d66181 | ||
|
|
0d7cf3fca2 | ||
|
|
8166784ba2 | ||
|
|
6d36a28cdc | ||
|
|
88548fcbe6 | ||
|
|
06debf5e14 | ||
|
|
ede7bd7b4b | ||
|
|
1c4eba41b7 | ||
|
|
313ff047df | ||
|
|
74ce8892c4 | ||
|
|
5817b81289 | ||
|
|
847d8d824f | ||
|
|
8a2b712287 | ||
|
|
117949b8d9 | ||
|
|
e78500feef | ||
|
|
d49de7bdf8 | ||
|
|
daaaa5f1be | ||
|
|
ee0ac7f9c0 | ||
|
|
e6fea1d132 | ||
|
|
749962a1db | ||
|
|
3229b9a4a6 | ||
|
|
b80010b1a1 | ||
|
|
938472bf1f | ||
|
|
64d0217456 | ||
|
|
d126c2bf80 | ||
|
|
b646485388 | ||
|
|
6b9d81a746 | ||
|
|
513ba636af | ||
|
|
2ae670e0e4 | ||
|
|
779f511fbf | ||
|
|
35d845cea5 | ||
|
|
dc36572fbf | ||
|
|
cba5c7bcac | ||
|
|
70db44f964 | ||
|
|
077fb3ab30 | ||
|
|
b65ae073d2 | ||
|
|
480113a4fe | ||
|
|
b1734b0d38 | ||
|
|
84c838cde1 | ||
|
|
0a3820a487 | ||
|
|
dd28959916 | ||
|
|
12f51bad5f | ||
|
|
4c8cafd6a6 | ||
|
|
152a9b6adf | ||
|
|
d47c980509 | ||
|
|
7f124cfe93 | ||
|
|
6901b2639d | ||
|
|
16d75a7c7b | ||
|
|
de68ef4548 | ||
|
|
f4639c418f | ||
|
|
24da30ab74 | ||
|
|
4be410cc4f | ||
|
|
cd1117515b | ||
|
|
3f550bc0ec | ||
|
|
706410e693 | ||
|
|
3131dba039 | ||
|
|
6bfa66c9ff | ||
|
|
6eee787493 | ||
|
|
30c77d8137 | ||
|
|
707e85ebcf | ||
|
|
09ada20ce8 | ||
|
|
9068bdacae | ||
|
|
155f9f80fe | ||
|
|
2056e9b740 | ||
|
|
44be433d44 | ||
|
|
002e921ede | ||
|
|
3098f35537 | ||
|
|
d7dee225fc | ||
|
|
c31bff7e57 | ||
|
|
5d199587a3 | ||
|
|
ececa65c78 | ||
|
|
7a400a7a79 | ||
|
|
2a0094def7 | ||
|
|
48471b7210 | ||
|
|
480c6e7c09 | ||
|
|
da77f99df4 | ||
|
|
ae4a78b298 | ||
|
|
da6511eba9 |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -11,12 +11,12 @@
|
||||
/packages/live-preview*/src/ @jacobsfletch
|
||||
/packages/plugin-stripe/src/ @jacobsfletch
|
||||
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
|
||||
/packages/richtext-*/src/ @AlessioGr @GermanJablo
|
||||
/packages/richtext-*/src/ @AlessioGr
|
||||
/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 @GermanJablo
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Templates
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
### Build Files
|
||||
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Root
|
||||
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -283,6 +283,7 @@ jobs:
|
||||
- fields-relationship
|
||||
- fields__collections__Array
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Blocks#config.blockreferences.ts
|
||||
- fields__collections__Checkbox
|
||||
- fields__collections__Collapsible
|
||||
- fields__collections__ConditionalLogic
|
||||
@@ -293,6 +294,7 @@ 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
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -19,6 +19,7 @@
|
||||
// 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"]
|
||||
},
|
||||
|
||||
@@ -1,550 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Customizing CSS & SCSS
|
||||
label: Customizing CSS
|
||||
order: 80
|
||||
order: 50
|
||||
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](./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.
|
||||
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.
|
||||
</Banner>
|
||||
|
||||
### Specificity rules
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Document Locking
|
||||
label: Document Locking
|
||||
order: 90
|
||||
order: 40
|
||||
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
|
||||
---
|
||||
|
||||
@@ -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](./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](../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.
|
||||
|
||||
To customize View Metadata, use the `meta` key within your View Config:
|
||||
|
||||
|
||||
@@ -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](./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 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 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](./components).
|
||||
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).
|
||||
</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](./components). |
|
||||
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
|
||||
| **`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](./views).
|
||||
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).
|
||||
</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](./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](../custom-components/custom-views) for more information.
|
||||
</Banner>
|
||||
|
||||
## I18n
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Managing User Preferences
|
||||
label: Preferences
|
||||
order: 70
|
||||
order: 60
|
||||
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';
|
||||
|
||||
const CustomComponent = (props) => {
|
||||
export function CustomComponent() {
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
|
||||
// Store the last used colors in local state
|
||||
@@ -154,8 +154,6 @@ const CustomComponent = (props) => {
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomComponent;
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Preview
|
||||
label: Preview
|
||||
order: 50
|
||||
order: 30
|
||||
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
|
||||
---
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
title: React Hooks
|
||||
label: React Hooks
|
||||
order: 40
|
||||
order: 50
|
||||
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](./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.
|
||||
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.
|
||||
|
||||
<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](./components#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](../custom-components/overview#client-components).
|
||||
</Banner>
|
||||
|
||||
## useField
|
||||
@@ -875,7 +875,7 @@ const Greeting: React.FC = () => {
|
||||
|
||||
## useConfig
|
||||
|
||||
Used to retrieve the Payload [Client Config](./components#accessing-the-payload-config).
|
||||
Used to retrieve the Payload [Client Config](../custom-components/overview#accessing-the-payload-config).
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -1113,5 +1113,40 @@ 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])
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -1,375 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -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](../admin/components), 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](../custom-components/overview), 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](../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.
|
||||
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.
|
||||
|
||||
To override Collection Components, use the `admin.components` property in your Collection Config:
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
export const MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: { // highlight-line
|
||||
@@ -152,23 +152,47 @@ export const MyCollection: SanitizedCollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| 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). |
|
||||
| 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). |
|
||||
|
||||
<Banner type="success">
|
||||
**Note:**
|
||||
For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).
|
||||
For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
### Pagination
|
||||
@@ -232,10 +256,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
|
||||
|
||||
|
||||
@@ -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](../admin/components) 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](../custom-components/overview) and need to access Environment Variables from the client-side, you can do so by prefixing them with `NEXT_PUBLIC_`.
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
|
||||
@@ -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](../admin/components), 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](../custom-components/overview), 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](../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.
|
||||
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.
|
||||
|
||||
To override Global Components, use the `admin.components` property in your Global Config:
|
||||
|
||||
@@ -146,17 +146,42 @@ export const MyGlobal: SanitizedGlobalConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
#### General
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`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). |
|
||||
| `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). |
|
||||
|
||||
<Banner type="success">
|
||||
**Note:**
|
||||
For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).
|
||||
For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
## GraphQL
|
||||
@@ -167,9 +192,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
|
||||
|
||||
|
||||
@@ -10,7 +10,13 @@ The [Admin Panel](../admin/overview) is translated in over [30 languages and cou
|
||||
|
||||
By default, Payload comes preinstalled with English, but you can easily load other languages into your own application. Languages are automatically detected based on the request. If no language is detected, or if the user's language is not yet supported by your application, English will be chosen.
|
||||
|
||||
To configure I18n, use the `i18n` key in your [Payload Config](./overview):
|
||||
To add I18n to your project, you first need to install the `@payloadcms/translations` package:
|
||||
|
||||
```bash
|
||||
pnpm install @payloadcms/translations
|
||||
```
|
||||
|
||||
Once installed, it can be configured using the `i18n` key in your [Payload Config](./overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
@@ -49,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
|
||||
|
||||
@@ -166,7 +172,11 @@ export const Articles: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
## Node
|
||||
## Changing Languages
|
||||
|
||||
Users can change their preferred language in their account settings or by otherwise manipulating their [User Preferences](../admin/preferences).
|
||||
|
||||
## Node.js#node
|
||||
|
||||
Payload's backend sets the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language.
|
||||
|
||||
@@ -174,7 +184,7 @@ Anywhere in your Payload app that you have access to the `req` object, you can a
|
||||
|
||||
## TypeScript
|
||||
|
||||
In order to use custom translations in your project, you need to provide the types for the translations.
|
||||
In order to use [Custom Translations](#custom-translations) in your project, you need to provide the types for the translations.
|
||||
|
||||
Here we create a shareable translations object. We will import this in both our custom components and in our Payload config.
|
||||
|
||||
@@ -217,7 +227,7 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
Import the shared translation types to use in your [Custom Component](../admin/components):
|
||||
Import the shared translation types to use in your [Custom Component](../custom-components/overview):
|
||||
|
||||
```ts
|
||||
// <rootDir>/components/MyComponent.tsx
|
||||
@@ -253,4 +263,3 @@ const field: Field = {
|
||||
) => t('fields:addLabel'),
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -77,11 +77,12 @@ 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. |
|
||||
| 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). |
|
||||
|
||||
### Locales
|
||||
|
||||
@@ -100,6 +101,35 @@ 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.
|
||||
|
||||
@@ -87,6 +87,7 @@ 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). |
|
||||
@@ -103,7 +104,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
Some properties are removed from the client-side bundle. [More details](../admin/components#accessing-the-payload-config).
|
||||
Some properties are removed from the client-side bundle. [More details](../custom-components/overview#accessing-the-payload-config).
|
||||
</Banner>
|
||||
|
||||
### Typescript Config
|
||||
|
||||
49
docs/custom-components/custom-providers.mdx
Normal file
49
docs/custom-components/custom-providers.mdx
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
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>
|
||||
364
docs/custom-components/custom-views.mdx
Normal file
364
docs/custom-components/custom-views.mdx
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
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)._
|
||||
186
docs/custom-components/document-views.mdx
Normal file
186
docs/custom-components/document-views.mdx
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
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>
|
||||
)
|
||||
}
|
||||
```
|
||||
463
docs/custom-components/edit-view.mdx
Normal file
463
docs/custom-components/edit-view.mdx
Normal file
@@ -0,0 +1,463 @@
|
||||
---
|
||||
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" />
|
||||
)
|
||||
}
|
||||
```
|
||||
387
docs/custom-components/list-view.mdx
Normal file
387
docs/custom-components/list-view.mdx
Normal file
@@ -0,0 +1,387 @@
|
||||
---
|
||||
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>
|
||||
)
|
||||
}
|
||||
```
|
||||
491
docs/custom-components/overview.mdx
Normal file
491
docs/custom-components/overview.mdx
Normal file
@@ -0,0 +1,491 @@
|
||||
---
|
||||
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>
|
||||
485
docs/custom-components/root-components.mdx
Normal file
485
docs/custom-components/root-components.mdx
Normal file
@@ -0,0 +1,485 @@
|
||||
---
|
||||
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>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -6,7 +6,7 @@ desc: The Blocks Field is a great layout build and can be used to construct any
|
||||
keywords: blocks, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Blocks Field is **incredibly powerful** storing an array of objects based on the fields that your define, where each item in the array is a "block" with its own unique schema.
|
||||
The Blocks Field is **incredibly powerful**, storing an array of objects based on the fields that you define, where each item in the array is a "block" with its own unique schema.
|
||||
|
||||
Blocks are a great way to create a flexible content model that can be used to build a wide variety of content types, including:
|
||||
|
||||
@@ -64,7 +64,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Blocks Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Blocks Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
@@ -295,6 +295,70 @@ 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:
|
||||
|
||||
@@ -54,7 +54,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Code Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Code Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -44,7 +44,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Collapsible Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Collapsible Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -51,7 +51,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Date Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Date Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -51,7 +51,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Email Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Email Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -55,7 +55,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Group Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Group Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -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. |
|
||||
| **`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) |
|
||||
| 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) |
|
||||
|
||||
_* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -177,6 +177,35 @@ 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
|
||||
@@ -198,7 +227,8 @@ 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 db.findOne('categories', {
|
||||
const result = await payload.find({
|
||||
collection: 'categories',
|
||||
where: {
|
||||
title: {
|
||||
equals: 'My Category'
|
||||
@@ -218,6 +248,25 @@ const result = await db.findOne('categories', {
|
||||
})
|
||||
```
|
||||
|
||||
<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
|
||||
|
||||
@@ -53,7 +53,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the JSON Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the JSON Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -56,7 +56,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Number Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Number Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -57,7 +57,7 @@ There are three main categories of fields in Payload:
|
||||
- [Presentational Fields](#presentational-fields)
|
||||
- [Virtual Fields](#virtual-fields)
|
||||
|
||||
To begin writing fields, first determine which [Field Type](#field-types) best supports your application. Then to author your field accordingly using the [Field Options](#field-options) for your chosen field type.
|
||||
To begin writing fields, first determine which [Field Type](#field-types) best supports your application. Then author your field accordingly using the [Field Options](#field-options) for your chosen field type.
|
||||
|
||||
### Data Fields
|
||||
|
||||
@@ -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](../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.
|
||||
**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.
|
||||
</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](../admin/components) that you define. |
|
||||
| **`components`** | All Field Components can be swapped out for [Custom Components](../custom-components/overview) 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 { SanitizedCollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
export const MyCollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
@@ -473,7 +473,7 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Reminder:** To replace the Field Description with a [Custom Component](../admin/components), use the `admin.components.Description` property. [More details](#description).
|
||||
**Reminder:** To replace the Field Description with a [Custom Component](../custom-components/overview), 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 { SanitizedCollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
export const MyCollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
@@ -618,7 +618,7 @@ export const CollectionConfig: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
*For details on how to build Custom Components, see [Building Custom Components](../admin/components#building-custom-components).*
|
||||
*For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#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](../admin/components#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](../custom-components/overview#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](../admin/components#building-custom-components).
|
||||
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#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](../admin/components#building-custom-components).
|
||||
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#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](../admin/components#building-custom-components).
|
||||
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#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](../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.
|
||||
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.
|
||||
|
||||
To add a Description Component to a field, use the `admin.components.Description` property in your Field Config:
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
export const MyCollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
@@ -813,7 +813,7 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
|
||||
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](../admin/components#building-custom-components).
|
||||
For details on how to build a Custom Components themselves, see [Building Custom Components](../custom-components/overview#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](../admin/components#building-custom-components).
|
||||
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#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](../admin/components#building-custom-components).
|
||||
For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#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 { SanitizedCollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
export const MyCollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
@@ -930,7 +930,7 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
|
||||
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](../admin/components#building-custom-components).
|
||||
For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#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'
|
||||
```
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: The Point field type stores coordinates in the database. Learn how to use
|
||||
keywords: point, geolocation, geospatial, geojson, 2dsphere, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Point Field saves a pair of coordinates in the database and assigns an index for location related queries. The data structure in the database matches the GeoJSON structure to represent point. The Payload APIs simplifies the object data to only the [longitude, latitude] location.
|
||||
The Point Field saves a pair of coordinates in the database and assigns an index for location related queries. The data structure in the database matches the GeoJSON structure to represent point. The Payload API simplifies the object data to only the [longitude, latitude] location.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/point.png"
|
||||
|
||||
@@ -66,7 +66,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Radio Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Radio Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: The Relationship field provides the ability to relate documents together.
|
||||
keywords: relationship, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Relationship Field is one of the most powerful fields Payload features. It provides for the ability to easily relate documents together.
|
||||
The Relationship Field is one of the most powerful fields Payload features. It provides the ability to easily relate documents together.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/relationship.png"
|
||||
@@ -71,8 +71,7 @@ _* An asterisk denotes that a property is required._
|
||||
</Banner>
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Relationship Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To the appearance and behavior of the Relationship Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
@@ -136,21 +135,19 @@ Note: If `sortOptions` is not defined, the default sorting behavior of the Relat
|
||||
|
||||
## Filtering relationship options
|
||||
|
||||
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both
|
||||
for validating input and filtering available relationships in the UI.
|
||||
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both for validating input and filtering available relationships in the UI.
|
||||
|
||||
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to
|
||||
prevent all, or a `Where` query. When using a function, it will be
|
||||
called with an argument object with the following properties:
|
||||
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to prevent all, or a `Where` query. When using a function, it will be called with an argument object with the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property |
|
||||
| `data` | An object containing the full collection or global document currently being edited |
|
||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field |
|
||||
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation |
|
||||
| `user` | An object containing the currently authenticated user |
|
||||
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
|
||||
| Property | Description |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `blockData` | The data of the nearest parent block. Will be `undefined` if the field is not within a block or when called on a `Filter` component within the list view. |
|
||||
| `data` | An object containing the full collection or global document currently being edited. Will be an empty object when called on a `Filter` component within the list view. |
|
||||
| `id` | The `id` of the current document being edited. Will be `undefined` during the `create` operation or when called on a `Filter` component within the list view. |
|
||||
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property. |
|
||||
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
|
||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. Will be an emprt object when called on a `Filter` component within the list view. |
|
||||
| `user` | An object containing the currently authenticated user. |
|
||||
|
||||
## Example
|
||||
|
||||
@@ -296,7 +293,7 @@ The `hasMany` tells Payload that there may be more than one collection saved to
|
||||
}
|
||||
```
|
||||
|
||||
To save the to `hasMany` relationship field we need to send an array of IDs:
|
||||
To save to the `hasMany` relationship field we need to send an array of IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Select Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Select Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -56,7 +56,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Text Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Text Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -53,7 +53,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Textarea Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Textarea Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Context allows you to pass in extra data that can be shared between hooks
|
||||
keywords: hooks, context, payload context, payloadcontext, data, extra data, shared data, shared, extra
|
||||
---
|
||||
|
||||
The `context` object is used to share data across different Hooks. This persists throughout the entire lifecycle of a request and is available within every Hook. By setting properties to `req.context`, you can effectively logic across multiple Hooks.
|
||||
The `context` object is used to share data across different Hooks. This persists throughout the entire lifecycle of a request and is available within every Hook. By setting properties to `req.context`, you can effectively share logic across multiple Hooks.
|
||||
|
||||
## When To Use Context
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ The following arguments are provided to all Field Hooks:
|
||||
| **`schemaPath`** | The path of the [Field](../fields/overview) in the schema. |
|
||||
| **`siblingData`** | The data of sibling fields adjacent to the field that the Hook is running against. |
|
||||
| **`siblingDocWithLocales`** | The sibling data of the Document with all [Locales](../configuration/localization). |
|
||||
| **`siblingFields`** | The sibling fields of the field which the hook is running against.
|
||||
|
|
||||
| **`value`** | The value of the [Field](../fields/overview). |
|
||||
|
||||
<Banner type="success">
|
||||
|
||||
@@ -345,8 +345,7 @@ const result = await payload.login({
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'rip',
|
||||
},
|
||||
req: req, // pass a Request object to be provided to all hooks
|
||||
res: res, // used to automatically set an HTTP-only auth cookie
|
||||
req: req, // optional, pass a Request object to be provided to all hooks
|
||||
depth: 2,
|
||||
locale: 'en',
|
||||
fallbackLocale: false,
|
||||
@@ -384,8 +383,7 @@ 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, // pass a Request object to be provided to all hooks
|
||||
res: res, // used to automatically set an HTTP-only auth cookie
|
||||
req: req, // optional, pass a Request object to be provided to all hooks
|
||||
})
|
||||
```
|
||||
|
||||
@@ -399,7 +397,7 @@ const result = await payload.unlock({
|
||||
// required
|
||||
email: 'dev@payloadcms.com',
|
||||
},
|
||||
req: req, // pass a Request object to be provided to all hooks
|
||||
req: req, // optional, pass a Request object to be provided to all hooks
|
||||
overrideAccess: true,
|
||||
})
|
||||
```
|
||||
|
||||
@@ -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/admin/components#component-paths).
|
||||
For more details, see the [Documentation](https://payloadcms.com/docs/custom-components/overview#component-paths).
|
||||
|
||||
1. All Custom Components are now server-rendered by default, and therefore, cannot use state or hooks directly. If you’re 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/admin/components#client-components).
|
||||
For more details, see the [Documentation](https://payloadcms.com/docs/custom-components/overview#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/components#accessing-the-payload-config).
|
||||
For more details, see the [Documentation](https://payloadcms.com/docs/admin/custom-components/overview#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`.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Plugins provide a great way to modularize Payload functionalities into eas
|
||||
keywords: plugins, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point. This is especially useful is sharing your work across multiple projects or with the greater Payload community.
|
||||
Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point. This is especially useful for sharing your work across multiple projects or with the greater Payload community.
|
||||
|
||||
There are many [Official Plugins](#official-plugins) available that solve for some of the most common uses cases, such as the [Form Builder Plugin](./form-builder) or [SEO Plugin](./seo). There are also [Community Plugins](#community-plugins) available, maintained entirely by contributing members. To extend Payload's functionality in some other way, you can easily [build your own plugin](./build-your-own).
|
||||
|
||||
|
||||
@@ -101,8 +101,7 @@ 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 <a href="/docs/configuration/overview">indexSortableFields</a>
|
||||
configuration option.
|
||||
fields that may be sorted in the admin UI use the [indexSortableFields](/docs/configuration/overview) option.
|
||||
|
||||
## File storage
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ 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 }) => {
|
||||
|
||||
@@ -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/components#component-paths) to the client feature like this:
|
||||
Inside of your server feature, you can provide an [import path](/docs/admin/custom-components/overview#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.js'
|
||||
import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext'
|
||||
import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
|
||||
@@ -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](../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.
|
||||
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.
|
||||
|
||||
## Types generation script
|
||||
|
||||
|
||||
@@ -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](../admin/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](../custom-components/custom-views), [GraphQL queries / mutations](../graphql/overview) or anything else.
|
||||
|
||||
## Config Types
|
||||
|
||||
|
||||
@@ -88,27 +88,29 @@ 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. |
|
||||
| **`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. |
|
||||
|
||||
|
||||
### Payload-wide Upload Options
|
||||
|
||||
@@ -19,9 +19,11 @@ export const defaultESLintIgnores = [
|
||||
'**/build/',
|
||||
'**/node_modules/',
|
||||
'**/temp/',
|
||||
'**/packages/*.spec.ts',
|
||||
'packages/**/*.spec.ts',
|
||||
'next-env.d.ts',
|
||||
'**/app',
|
||||
'src/**/*.spec.ts',
|
||||
'**/jest.setup.js',
|
||||
]
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
@@ -29,10 +31,7 @@ export const defaultESLintIgnores = [
|
||||
export const rootParserOptions = {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
projectService: {
|
||||
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 40,
|
||||
allowDefaultProject: ['scripts/*.ts', '*.js', '*.mjs', '*.d.ts'],
|
||||
},
|
||||
projectService: true,
|
||||
}
|
||||
|
||||
/** @type {Config[]} */
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface Props {
|
||||
onLoad?: () => void
|
||||
priority?: boolean // for NextImage only
|
||||
ref?: Ref<HTMLImageElement | HTMLVideoElement | null>
|
||||
resource?: MediaType | string | number // for Payload media
|
||||
resource?: MediaType | string | number | null // for Payload media
|
||||
size?: string // for NextImage only
|
||||
src?: StaticImageData // for static media
|
||||
videoClassName?: string
|
||||
|
||||
@@ -10,6 +10,9 @@ 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: [
|
||||
|
||||
@@ -27,6 +27,8 @@ 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 [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -5,6 +5,7 @@ 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(
|
||||
@@ -23,9 +24,11 @@ export const count: Count = async function count(
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { CountOptions } from 'mongodb'
|
||||
import type { CountGlobalVersions } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionGlobalFields, 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(
|
||||
@@ -23,9 +24,14 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.globals.config.find((each) => each.slug === global),
|
||||
true,
|
||||
),
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { CountOptions } from 'mongodb'
|
||||
import type { CountVersions } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionCollectionFields, 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(
|
||||
@@ -23,9 +24,14 @@ export const countVersions: CountVersions = async function countVersions(
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ 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(
|
||||
@@ -14,8 +15,10 @@ export const deleteMany: DeleteMany = async function deleteMany(
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
payload: this.payload,
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
@@ -21,12 +22,18 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
payload: this.payload,
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOneAndDelete(query, options).lean()
|
||||
const doc = await Model.findOneAndDelete(query, options)?.lean()
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DeleteVersions } from 'payload'
|
||||
import { buildVersionCollectionFields, 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(
|
||||
@@ -12,9 +13,14 @@ export const deleteVersions: DeleteVersions = async function deleteVersions(
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await VersionsModel.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
@@ -50,9 +51,11 @@ export const find: Find = async function find(
|
||||
})
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
@@ -14,20 +15,22 @@ 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: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields,
|
||||
fields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
globalSlug: slug,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where: combineQueries({ globalType: { equals: slug } }, where),
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
@@ -46,10 +47,10 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
})
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
globalSlug: global,
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: versionFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
@@ -20,9 +21,11 @@ export const findOne: FindOne = async function findOne(
|
||||
session,
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
@@ -41,9 +42,12 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
})
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -58,7 +62,7 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
pagination,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
fields,
|
||||
select,
|
||||
}),
|
||||
sort,
|
||||
|
||||
@@ -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/buildQuery.js'
|
||||
import { getBuildQueryPlugin } from './queries/getBuildQueryPlugin.js'
|
||||
import { getDBName } from './utilities/getDBName.js'
|
||||
|
||||
export const init: Init = function init(this: MongooseAdapter) {
|
||||
@@ -26,15 +26,19 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
|
||||
const versionCollectionFields = buildVersionCollectionFields(this.payload.config, collection)
|
||||
|
||||
const versionSchema = buildSchema(this.payload, versionCollectionFields, {
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
indexSortableFields: this.payload.config.indexSortableFields,
|
||||
options: {
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
const versionSchema = buildSchema({
|
||||
buildSchemaOptions: {
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
indexSortableFields: this.payload.config.indexSortableFields,
|
||||
options: {
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
},
|
||||
...schemaOptions,
|
||||
},
|
||||
...schemaOptions,
|
||||
configFields: versionCollectionFields,
|
||||
payload: this.payload,
|
||||
})
|
||||
|
||||
versionSchema.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true }).plugin(
|
||||
@@ -77,14 +81,18 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
|
||||
const versionGlobalFields = buildVersionGlobalFields(this.payload.config, global)
|
||||
|
||||
const versionSchema = buildSchema(this.payload, versionGlobalFields, {
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
indexSortableFields: this.payload.config.indexSortableFields,
|
||||
options: {
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
const versionSchema = buildSchema({
|
||||
buildSchemaOptions: {
|
||||
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(
|
||||
|
||||
@@ -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/buildQuery.js'
|
||||
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
|
||||
import { buildSchema } from './buildSchema.js'
|
||||
|
||||
export const buildCollectionSchema = (
|
||||
@@ -12,14 +12,20 @@ export const buildCollectionSchema = (
|
||||
payload: Payload,
|
||||
schemaOptions = {},
|
||||
): Schema => {
|
||||
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,
|
||||
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,
|
||||
},
|
||||
},
|
||||
configFields: collection.fields,
|
||||
payload,
|
||||
})
|
||||
|
||||
if (Array.isArray(collection.upload.filenameCompoundIndex)) {
|
||||
@@ -34,16 +40,14 @@ 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) {
|
||||
if (
|
||||
Object.keys(collection.joins).length > 0 ||
|
||||
Object.keys(collection.polymorphicJoins).length > 0
|
||||
) {
|
||||
schema.plugin(mongooseAggregatePaginate)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import mongoose from 'mongoose'
|
||||
|
||||
import type { GlobalModel } from '../types.js'
|
||||
|
||||
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
|
||||
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
|
||||
import { buildSchema } from './buildSchema.js'
|
||||
|
||||
export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
|
||||
@@ -19,10 +19,14 @@ 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(payload, globalConfig.fields, {
|
||||
options: {
|
||||
minimize: false,
|
||||
const globalSchema = buildSchema({
|
||||
buildSchemaOptions: {
|
||||
options: {
|
||||
minimize: false,
|
||||
},
|
||||
},
|
||||
configFields: globalConfig.fields,
|
||||
payload,
|
||||
})
|
||||
Globals.discriminator(globalConfig.slug, globalSchema)
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mon
|
||||
import mongoose from 'mongoose'
|
||||
import {
|
||||
type ArrayField,
|
||||
type Block,
|
||||
type BlocksField,
|
||||
type CheckboxField,
|
||||
type CodeField,
|
||||
@@ -32,9 +31,9 @@ import {
|
||||
} from 'payload'
|
||||
import {
|
||||
fieldAffectsData,
|
||||
fieldIsLocalized,
|
||||
fieldIsPresentationalOnly,
|
||||
fieldIsVirtual,
|
||||
fieldShouldBeLocalized,
|
||||
tabHasName,
|
||||
} from 'payload/shared'
|
||||
|
||||
@@ -51,6 +50,7 @@ type FieldSchemaGenerator = (
|
||||
schema: Schema,
|
||||
config: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
parentIsLocalized: boolean,
|
||||
) => void
|
||||
|
||||
/**
|
||||
@@ -62,7 +62,15 @@ const formatDefaultValue = (field: FieldAffectingData) =>
|
||||
? field.defaultValue
|
||||
: undefined
|
||||
|
||||
const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => {
|
||||
const formatBaseSchema = ({
|
||||
buildSchemaOptions,
|
||||
field,
|
||||
parentIsLocalized,
|
||||
}: {
|
||||
buildSchemaOptions: BuildSchemaOptions
|
||||
field: FieldAffectingData
|
||||
parentIsLocalized: boolean
|
||||
}) => {
|
||||
const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions
|
||||
const schema: SchemaTypeOptions<unknown> = {
|
||||
default: formatDefaultValue(field),
|
||||
@@ -73,7 +81,7 @@ const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSc
|
||||
|
||||
if (
|
||||
schema.unique &&
|
||||
(field.localized ||
|
||||
(fieldShouldBeLocalized({ field, parentIsLocalized }) ||
|
||||
draftsEnabled ||
|
||||
(fieldAffectsData(field) &&
|
||||
field.type !== 'group' &&
|
||||
@@ -94,8 +102,13 @@ const localizeSchema = (
|
||||
entity: NonPresentationalField | Tab,
|
||||
schema,
|
||||
localization: false | SanitizedLocalizationConfig,
|
||||
parentIsLocalized: boolean,
|
||||
) => {
|
||||
if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) {
|
||||
if (
|
||||
fieldShouldBeLocalized({ field: entity, parentIsLocalized }) &&
|
||||
localization &&
|
||||
Array.isArray(localization.locales)
|
||||
) {
|
||||
return {
|
||||
type: localization.localeCodes.reduce(
|
||||
(localeSchema, locale) => ({
|
||||
@@ -112,11 +125,13 @@ const localizeSchema = (
|
||||
return schema
|
||||
}
|
||||
|
||||
export const buildSchema = (
|
||||
payload: Payload,
|
||||
configFields: Field[],
|
||||
buildSchemaOptions: BuildSchemaOptions = {},
|
||||
): Schema => {
|
||||
export const buildSchema = (args: {
|
||||
buildSchemaOptions: BuildSchemaOptions
|
||||
configFields: Field[]
|
||||
parentIsLocalized?: boolean
|
||||
payload: Payload
|
||||
}): Schema => {
|
||||
const { buildSchemaOptions = {}, configFields, parentIsLocalized, payload } = args
|
||||
const { allowIDField, options } = buildSchemaOptions
|
||||
let fields = {}
|
||||
|
||||
@@ -145,7 +160,7 @@ export const buildSchema = (
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(field, schema, payload, buildSchemaOptions)
|
||||
addFieldSchema(field, schema, payload, buildSchemaOptions, parentIsLocalized)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -154,96 +169,121 @@ export const buildSchema = (
|
||||
}
|
||||
|
||||
const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
array: (
|
||||
field: ArrayField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
) => {
|
||||
array: (field: ArrayField, schema, payload, buildSchemaOptions, parentIsLocalized) => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: [
|
||||
buildSchema(payload, field.fields, {
|
||||
allowIDField: true,
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
options: {
|
||||
_id: false,
|
||||
id: false,
|
||||
minimize: false,
|
||||
buildSchema({
|
||||
buildSchemaOptions: {
|
||||
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),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
blocks: (
|
||||
field: BlocksField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
blocks: (field: BlocksField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const fieldSchema = {
|
||||
type: [new mongoose.Schema({}, { _id: false, discriminatorKey: 'blockType' })],
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, fieldSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
fieldSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
|
||||
field.blocks.forEach((blockItem: Block) => {
|
||||
;(field.blockReferences ?? field.blocks).forEach((blockItem) => {
|
||||
const blockSchema = new mongoose.Schema({}, { _id: false, id: false })
|
||||
|
||||
blockItem.fields.forEach((blockField) => {
|
||||
const block = typeof blockItem === 'string' ? payload.blocks[blockItem] : blockItem
|
||||
|
||||
block.fields.forEach((blockField) => {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(blockField, blockSchema, payload, buildSchemaOptions)
|
||||
addFieldSchema(
|
||||
blockField,
|
||||
blockSchema,
|
||||
payload,
|
||||
buildSchemaOptions,
|
||||
parentIsLocalized || field.localized,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (field.localized && payload.config.localization) {
|
||||
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && 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(blockItem.slug, blockSchema)
|
||||
schema.path(`${field.name}.${localeCode}`).discriminator(block.slug, blockSchema)
|
||||
})
|
||||
} else {
|
||||
// @ts-expect-error Possible incorrect typing in mongoose types, this works
|
||||
schema.path(field.name).discriminator(blockItem.slug, blockSchema)
|
||||
schema.path(field.name).discriminator(block.slug, blockSchema)
|
||||
}
|
||||
})
|
||||
},
|
||||
checkbox: (
|
||||
field: CheckboxField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
schema,
|
||||
payload,
|
||||
buildSchemaOptions,
|
||||
parentIsLocalized,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }
|
||||
const baseSchema = {
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: Boolean,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
code: (
|
||||
field: CodeField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
code: (field: CodeField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: String,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
collapsible: (
|
||||
field: CollapsibleField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
schema,
|
||||
payload,
|
||||
buildSchemaOptions,
|
||||
parentIsLocalized,
|
||||
): void => {
|
||||
field.fields.forEach((subField: Field) => {
|
||||
if (fieldIsVirtual(subField)) {
|
||||
@@ -253,41 +293,42 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(subField, schema, payload, buildSchemaOptions)
|
||||
addFieldSchema(subField, schema, payload, buildSchemaOptions, parentIsLocalized)
|
||||
}
|
||||
})
|
||||
},
|
||||
date: (
|
||||
field: DateField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }
|
||||
date: (field: DateField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: Date,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
email: (
|
||||
field: EmailField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
email: (field: EmailField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: String,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
group: (
|
||||
field: GroupField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions)
|
||||
group: (field: GroupField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
|
||||
|
||||
// carry indexSortableFields through to versions if drafts enabled
|
||||
const indexSortableFields =
|
||||
@@ -297,58 +338,63 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
|
||||
const baseSchema = {
|
||||
...formattedBaseSchema,
|
||||
type: buildSchema(payload, field.fields, {
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
indexSortableFields,
|
||||
options: {
|
||||
_id: false,
|
||||
id: false,
|
||||
minimize: false,
|
||||
type: buildSchema({
|
||||
buildSchemaOptions: {
|
||||
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),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
json: (
|
||||
field: JSONField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
json: (field: JSONField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
number: (
|
||||
field: NumberField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
number: (field: NumberField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: field.hasMany ? [Number] : Number,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
point: (
|
||||
field: PointField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
point: (field: PointField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema: SchemaTypeOptions<unknown> = {
|
||||
type: {
|
||||
type: String,
|
||||
@@ -363,12 +409,21 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
required: false,
|
||||
},
|
||||
}
|
||||
if (buildSchemaOptions.disableUnique && field.unique && field.localized) {
|
||||
if (
|
||||
buildSchemaOptions.disableUnique &&
|
||||
field.unique &&
|
||||
fieldShouldBeLocalized({ field, parentIsLocalized })
|
||||
) {
|
||||
baseSchema.coordinates.sparse = true
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
|
||||
if (field.index === true || field.index === undefined) {
|
||||
@@ -377,7 +432,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
indexOptions.sparse = true
|
||||
indexOptions.unique = true
|
||||
}
|
||||
if (field.localized && payload.config.localization) {
|
||||
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
|
||||
payload.config.localization.locales.forEach((locale) => {
|
||||
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
|
||||
})
|
||||
@@ -386,14 +441,9 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
}
|
||||
},
|
||||
radio: (
|
||||
field: RadioField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
radio: (field: RadioField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: String,
|
||||
enum: field.options.map((option) => {
|
||||
if (typeof option === 'object') {
|
||||
@@ -404,28 +454,34 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
relationship: (
|
||||
field: RelationshipField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
schema,
|
||||
payload,
|
||||
buildSchemaOptions,
|
||||
parentIsLocalized,
|
||||
) => {
|
||||
const hasManyRelations = Array.isArray(field.relationTo)
|
||||
let schemaToReturn: { [key: string]: any } = {}
|
||||
|
||||
const valueType = getRelationshipValueType(field, payload)
|
||||
|
||||
if (field.localized && payload.config.localization) {
|
||||
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
|
||||
schemaToReturn = {
|
||||
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
if (hasManyRelations) {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
_id: false,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
@@ -436,7 +492,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
} else {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: valueType,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
@@ -453,7 +509,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
} else if (hasManyRelations) {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
_id: false,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
@@ -471,7 +527,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
} else {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: valueType,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
@@ -490,25 +546,26 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
},
|
||||
richText: (
|
||||
field: RichTextField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
schema,
|
||||
payload,
|
||||
buildSchemaOptions,
|
||||
parentIsLocalized,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
row: (
|
||||
field: RowField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
row: (field: RowField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
field.fields.forEach((subField: Field) => {
|
||||
if (fieldIsVirtual(subField)) {
|
||||
return
|
||||
@@ -517,18 +574,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(subField, schema, payload, buildSchemaOptions)
|
||||
addFieldSchema(subField, schema, payload, buildSchemaOptions, parentIsLocalized)
|
||||
}
|
||||
})
|
||||
},
|
||||
select: (
|
||||
field: SelectField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
select: (field: SelectField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: String,
|
||||
enum: field.options.map((option) => {
|
||||
if (typeof option === 'object') {
|
||||
@@ -547,34 +599,40 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
field,
|
||||
field.hasMany ? [baseSchema] : baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
tabs: (
|
||||
field: TabsField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
tabs: (field: TabsField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
if (fieldIsVirtual(tab)) {
|
||||
return
|
||||
}
|
||||
const baseSchema = {
|
||||
type: buildSchema(payload, tab.fields, {
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
options: {
|
||||
_id: false,
|
||||
id: false,
|
||||
minimize: false,
|
||||
type: buildSchema({
|
||||
buildSchemaOptions: {
|
||||
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),
|
||||
[tab.name]: localizeSchema(
|
||||
tab,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
} else {
|
||||
tab.fields.forEach((subField: Field) => {
|
||||
@@ -584,58 +642,68 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(subField, schema, payload, buildSchemaOptions)
|
||||
addFieldSchema(
|
||||
subField,
|
||||
schema,
|
||||
payload,
|
||||
buildSchemaOptions,
|
||||
parentIsLocalized || tab.localized,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
text: (
|
||||
field: TextField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
text: (field: TextField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: field.hasMany ? [String] : String,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
textarea: (
|
||||
field: TextareaField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
schema,
|
||||
payload,
|
||||
buildSchemaOptions,
|
||||
parentIsLocalized,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
const baseSchema = {
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: String,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
},
|
||||
upload: (
|
||||
field: UploadField,
|
||||
schema: Schema,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
upload: (field: UploadField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
|
||||
const hasManyRelations = Array.isArray(field.relationTo)
|
||||
let schemaToReturn: { [key: string]: any } = {}
|
||||
|
||||
const valueType = getRelationshipValueType(field, payload)
|
||||
|
||||
if (field.localized && payload.config.localization) {
|
||||
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
|
||||
schemaToReturn = {
|
||||
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
if (hasManyRelations) {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
_id: false,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
@@ -646,7 +714,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
} else {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: valueType,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
@@ -663,7 +731,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
} else if (hasManyRelations) {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
_id: false,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
@@ -681,7 +749,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
} else {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
|
||||
type: valueType,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ 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
|
||||
@@ -47,7 +49,7 @@ const migrateModelWithBatching = async ({
|
||||
}
|
||||
|
||||
for (const doc of docs) {
|
||||
sanitizeRelationshipIDs({ config, data: doc, fields })
|
||||
sanitizeRelationshipIDs({ config, data: doc, fields, parentIsLocalized })
|
||||
}
|
||||
|
||||
await Model.collection.bulkWrite(
|
||||
@@ -80,6 +82,10 @@ 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
|
||||
}
|
||||
@@ -120,6 +126,7 @@ export async function migrateRelationshipsV2_V3({
|
||||
config,
|
||||
fields: collection.fields,
|
||||
Model: db.collections[collection.slug],
|
||||
parentIsLocalized: false,
|
||||
session,
|
||||
})
|
||||
|
||||
@@ -134,6 +141,7 @@ export async function migrateRelationshipsV2_V3({
|
||||
config,
|
||||
fields: buildVersionCollectionFields(config, collection),
|
||||
Model: db.versions[collection.slug],
|
||||
parentIsLocalized: false,
|
||||
session,
|
||||
})
|
||||
|
||||
@@ -159,7 +167,11 @@ 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(
|
||||
{
|
||||
@@ -181,6 +193,7 @@ export async function migrateRelationshipsV2_V3({
|
||||
config,
|
||||
fields: buildVersionGlobalFields(config, global),
|
||||
Model: db.versions[global.slug],
|
||||
parentIsLocalized: false,
|
||||
session,
|
||||
})
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export async function buildAndOrConditions({
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
payload,
|
||||
where,
|
||||
}: {
|
||||
@@ -14,6 +15,7 @@ export async function buildAndOrConditions({
|
||||
fields: FlattenedField[]
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
parentIsLocalized: boolean
|
||||
payload: Payload
|
||||
where: Where[]
|
||||
}): Promise<Record<string, unknown>[]> {
|
||||
@@ -29,6 +31,7 @@ export async function buildAndOrConditions({
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
payload,
|
||||
where: condition,
|
||||
})
|
||||
|
||||
@@ -1,62 +1,33 @@
|
||||
import type { FlattenedField, Payload, Where } from 'payload'
|
||||
import type { FlattenedField, Where } from 'payload'
|
||||
|
||||
import { QueryError } from 'payload'
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { parseParams } from './parseParams.js'
|
||||
|
||||
type GetBuildQueryPluginArgs = {
|
||||
export const buildQuery = async ({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
where,
|
||||
}: {
|
||||
adapter: MongooseAdapter
|
||||
collectionSlug?: string
|
||||
versionsFields?: FlattenedField[]
|
||||
}
|
||||
|
||||
export type BuildQueryArgs = {
|
||||
fields: FlattenedField[]
|
||||
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.
|
||||
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
|
||||
}
|
||||
}) => {
|
||||
const result = await parseParams({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
parentIsLocalized: false,
|
||||
payload: adapter.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function buildSearchParam({
|
||||
incomingPath,
|
||||
locale,
|
||||
operator,
|
||||
parentIsLocalized,
|
||||
payload,
|
||||
val,
|
||||
}: {
|
||||
@@ -39,6 +40,7 @@ export async function buildSearchParam({
|
||||
incomingPath: string
|
||||
locale?: string
|
||||
operator: string
|
||||
parentIsLocalized: boolean
|
||||
payload: Payload
|
||||
val: unknown
|
||||
}): Promise<SearchParam> {
|
||||
@@ -69,6 +71,7 @@ export async function buildSearchParam({
|
||||
name: 'id',
|
||||
type: idFieldType,
|
||||
} as FlattenedField,
|
||||
parentIsLocalized,
|
||||
path: '_id',
|
||||
})
|
||||
} else {
|
||||
@@ -78,6 +81,7 @@ export async function buildSearchParam({
|
||||
globalSlug,
|
||||
incomingPath: sanitizedPath,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -89,6 +93,7 @@ export async function buildSearchParam({
|
||||
hasCustomID,
|
||||
locale,
|
||||
operator,
|
||||
parentIsLocalized,
|
||||
path,
|
||||
payload,
|
||||
val,
|
||||
|
||||
@@ -7,6 +7,7 @@ type Args = {
|
||||
config: SanitizedConfig
|
||||
fields: FlattenedField[]
|
||||
locale: string
|
||||
parentIsLocalized?: boolean
|
||||
sort: Sort
|
||||
timestamps: boolean
|
||||
}
|
||||
@@ -22,6 +23,7 @@ export const buildSortParam = ({
|
||||
config,
|
||||
fields,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
sort,
|
||||
timestamps,
|
||||
}: Args): PaginateOptions['sort'] => {
|
||||
@@ -55,6 +57,7 @@ export const buildSortParam = ({
|
||||
config,
|
||||
fields,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
segments: sortProperty.split('.'),
|
||||
})
|
||||
acc[localizedProperty] = sortDirection
|
||||
|
||||
65
packages/db-mongodb/src/queries/getBuildQueryPlugin.ts
Normal file
65
packages/db-mongodb/src/queries/getBuildQueryPlugin.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { FlattenedField, SanitizedConfig } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
config: SanitizedConfig
|
||||
fields: FlattenedField[]
|
||||
locale: string
|
||||
parentIsLocalized: boolean
|
||||
result?: string
|
||||
segments: string[]
|
||||
}
|
||||
@@ -14,6 +15,7 @@ export const getLocalizedSortProperty = ({
|
||||
config,
|
||||
fields,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
result: incomingResult,
|
||||
segments: incomingSegments,
|
||||
}: Args): string => {
|
||||
@@ -35,10 +37,11 @@ export const getLocalizedSortProperty = ({
|
||||
|
||||
if (matchedField && !fieldIsPresentationalOnly(matchedField)) {
|
||||
let nextFields: FlattenedField[]
|
||||
let nextParentIsLocalized = parentIsLocalized
|
||||
const remainingSegments = [...segments]
|
||||
let localizedSegment = matchedField.name
|
||||
|
||||
if (matchedField.localized) {
|
||||
if (fieldShouldBeLocalized({ field: matchedField, parentIsLocalized })) {
|
||||
// Check to see if next segment is a locale
|
||||
if (segments.length > 0) {
|
||||
const nextSegmentIsLocale = config.localization.localeCodes.includes(remainingSegments[0])
|
||||
@@ -62,21 +65,30 @@ export const getLocalizedSortProperty = ({
|
||||
matchedField.type === 'array'
|
||||
) {
|
||||
nextFields = matchedField.flattenedFields
|
||||
if (!nextParentIsLocalized) {
|
||||
nextParentIsLocalized = matchedField.localized
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedField.type === 'blocks') {
|
||||
nextFields = matchedField.blocks.reduce((flattenedBlockFields, block) => {
|
||||
return [
|
||||
...flattenedBlockFields,
|
||||
...block.flattenedFields.filter(
|
||||
(blockField) =>
|
||||
(fieldAffectsData(blockField) &&
|
||||
blockField.name !== 'blockType' &&
|
||||
blockField.name !== 'blockName') ||
|
||||
!fieldAffectsData(blockField),
|
||||
),
|
||||
]
|
||||
}, [])
|
||||
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),
|
||||
),
|
||||
]
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
const result = incomingResult ? `${incomingResult}.${localizedSegment}` : localizedSegment
|
||||
@@ -86,6 +98,7 @@ export const getLocalizedSortProperty = ({
|
||||
config,
|
||||
fields: nextFields,
|
||||
locale,
|
||||
parentIsLocalized: nextParentIsLocalized,
|
||||
result,
|
||||
segments: remainingSegments,
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function parseParams({
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
payload,
|
||||
where,
|
||||
}: {
|
||||
@@ -19,6 +20,7 @@ export async function parseParams({
|
||||
fields: FlattenedField[]
|
||||
globalSlug?: string
|
||||
locale: string
|
||||
parentIsLocalized: boolean
|
||||
payload: Payload
|
||||
where: Where
|
||||
}): Promise<Record<string, unknown>> {
|
||||
@@ -40,6 +42,7 @@ export async function parseParams({
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
payload,
|
||||
where: condition,
|
||||
})
|
||||
@@ -63,6 +66,7 @@ export async function parseParams({
|
||||
incomingPath: relationOrPath,
|
||||
locale,
|
||||
operator,
|
||||
parentIsLocalized,
|
||||
payload,
|
||||
val: pathOperators[operator],
|
||||
})
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import type { FlattenedBlock, FlattenedField, Payload, RelationshipField } from 'payload'
|
||||
import type {
|
||||
FlattenedBlock,
|
||||
FlattenedBlocksField,
|
||||
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
|
||||
@@ -40,14 +48,18 @@ 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) {
|
||||
for (const block of field.blocks) {
|
||||
const field = getFieldFromSegments({ field: block, segments })
|
||||
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 (field) {
|
||||
return field
|
||||
}
|
||||
@@ -67,7 +79,7 @@ const getFieldFromSegments = ({
|
||||
}
|
||||
|
||||
segments.shift()
|
||||
return getFieldFromSegments({ field: foundField, segments })
|
||||
return getFieldFromSegments({ field: foundField, payload, segments })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +89,7 @@ export const sanitizeQueryValue = ({
|
||||
hasCustomID,
|
||||
locale,
|
||||
operator,
|
||||
parentIsLocalized,
|
||||
path,
|
||||
payload,
|
||||
val,
|
||||
@@ -87,11 +100,10 @@ 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, segments })
|
||||
const foundField = getFieldFromSegments({ field, payload, segments })
|
||||
|
||||
if (foundField) {
|
||||
field = foundField
|
||||
@@ -209,7 +221,11 @@ export const sanitizeQueryValue = ({
|
||||
|
||||
let localizedPath = path
|
||||
|
||||
if (field.localized && payload.config.localization && locale) {
|
||||
if (
|
||||
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
|
||||
payload.config.localization &&
|
||||
locale
|
||||
) {
|
||||
localizedPath = `${path}.${locale}`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
@@ -41,15 +42,17 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
|
||||
const combinedWhere = combineQueries({ latest: { equals: true } }, where)
|
||||
|
||||
const versionQuery = await VersionModel.buildQuery({
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
const versionQuery = await buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where: combinedWhere,
|
||||
})
|
||||
|
||||
const projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
fields,
|
||||
select,
|
||||
})
|
||||
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
|
||||
|
||||
@@ -35,7 +35,7 @@ import type {
|
||||
UploadField,
|
||||
} from 'payload'
|
||||
|
||||
import type { BuildQueryArgs } from './queries/buildQuery.js'
|
||||
import type { BuildQueryArgs } from './queries/getBuildQueryPlugin.js'
|
||||
|
||||
export interface CollectionModel
|
||||
extends Model<any>,
|
||||
|
||||
@@ -37,6 +37,10 @@ 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
|
||||
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
@@ -26,22 +27,23 @@ 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: buildVersionGlobalFields(this.payload.config, currentGlobal, true),
|
||||
fields: flattenedFields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await VersionModel.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
@@ -53,6 +55,10 @@ 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
|
||||
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
@@ -28,9 +29,11 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -48,6 +51,10 @@ 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)
|
||||
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
@@ -19,25 +20,28 @@ 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: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
fields: flattenedFields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await VersionModel.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
@@ -49,6 +53,10 @@ 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
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import type { PipelineStage } from 'mongoose'
|
||||
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
|
||||
import type {
|
||||
CollectionSlug,
|
||||
FlattenedField,
|
||||
JoinQuery,
|
||||
SanitizedCollectionConfig,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
import { fieldShouldBeLocalized } from 'payload/shared'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { buildQuery } from '../queries/buildQuery.js'
|
||||
import { buildSortParam } from '../queries/buildSortParam.js'
|
||||
|
||||
type BuildJoinAggregationArgs = {
|
||||
@@ -31,11 +40,16 @@ export const buildJoinAggregation = async ({
|
||||
query,
|
||||
versions,
|
||||
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
|
||||
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
|
||||
if (
|
||||
(Object.keys(collectionConfig.joins).length === 0 &&
|
||||
collectionConfig.polymorphicJoins.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 },
|
||||
@@ -54,10 +68,151 @@ 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
|
||||
}
|
||||
@@ -68,10 +223,17 @@ 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,
|
||||
@@ -95,6 +257,12 @@ export const buildJoinAggregation = async ({
|
||||
},
|
||||
]
|
||||
|
||||
if (page) {
|
||||
pipeline.push({
|
||||
$skip: (page - 1) * limitJoin,
|
||||
})
|
||||
}
|
||||
|
||||
if (limitJoin > 0) {
|
||||
pipeline.push({
|
||||
$limit: limitJoin + 1,
|
||||
@@ -148,7 +316,14 @@ export const buildJoinAggregation = async ({
|
||||
})
|
||||
} else {
|
||||
const localeSuffix =
|
||||
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
|
||||
fieldShouldBeLocalized({
|
||||
field: join.field,
|
||||
parentIsLocalized: join.parentIsLocalized,
|
||||
}) &&
|
||||
adapter.payload.config.localization &&
|
||||
locale
|
||||
? `.${locale}`
|
||||
: ''
|
||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${localeSuffix}`
|
||||
|
||||
let foreignField: string
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload'
|
||||
|
||||
import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared'
|
||||
import {
|
||||
deepCopyObjectSimple,
|
||||
fieldAffectsData,
|
||||
fieldShouldBeLocalized,
|
||||
getSelectMode,
|
||||
} from 'payload/shared'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
@@ -8,18 +13,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 (withinLocalizedField && config.localization) {
|
||||
if (parentIsLocalized && config.localization) {
|
||||
for (const locale of config.localization.localeCodes) {
|
||||
const localeDatabaseSchemaPath = databaseSchemaPath.replace('<locale>', locale)
|
||||
projection[`${localeDatabaseSchemaPath}${field.name}`] = true
|
||||
@@ -33,20 +38,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)) {
|
||||
@@ -56,8 +61,8 @@ const traverseFields = ({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
field,
|
||||
parentIsLocalized,
|
||||
projection,
|
||||
withinLocalizedField,
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -73,8 +78,8 @@ const traverseFields = ({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
field,
|
||||
parentIsLocalized,
|
||||
projection,
|
||||
withinLocalizedField,
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -86,14 +91,12 @@ const traverseFields = ({
|
||||
}
|
||||
|
||||
let fieldDatabaseSchemaPath = databaseSchemaPath
|
||||
let fieldWithinLocalizedField = withinLocalizedField
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
fieldDatabaseSchemaPath = `${databaseSchemaPath}${field.name}.`
|
||||
|
||||
if (field.localized) {
|
||||
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
fieldDatabaseSchemaPath = `${fieldDatabaseSchemaPath}<locale>.`
|
||||
fieldWithinLocalizedField = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,10 +114,10 @@ const traverseFields = ({
|
||||
adapter,
|
||||
databaseSchemaPath: fieldDatabaseSchemaPath,
|
||||
fields: field.flattenedFields,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
projection,
|
||||
select: fieldSelect,
|
||||
selectMode,
|
||||
withinLocalizedField: fieldWithinLocalizedField,
|
||||
})
|
||||
|
||||
break
|
||||
@@ -123,7 +126,8 @@ const traverseFields = ({
|
||||
case 'blocks': {
|
||||
const blocksSelect = select[field.name] as SelectType
|
||||
|
||||
for (const block of field.blocks) {
|
||||
for (const _block of field.blockReferences ?? field.blocks) {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
if (
|
||||
(selectMode === 'include' && blocksSelect[block.slug] === true) ||
|
||||
(selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined')
|
||||
@@ -132,11 +136,11 @@ const traverseFields = ({
|
||||
adapter,
|
||||
databaseSchemaPath: fieldDatabaseSchemaPath,
|
||||
fields: block.flattenedFields,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
projection,
|
||||
select: {},
|
||||
selectAllOnCurrentLevel: true,
|
||||
selectMode: 'include',
|
||||
withinLocalizedField: fieldWithinLocalizedField,
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -160,10 +164,10 @@ const traverseFields = ({
|
||||
adapter,
|
||||
databaseSchemaPath: fieldDatabaseSchemaPath,
|
||||
fields: block.flattenedFields,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
projection,
|
||||
select: blocksSelect[block.slug] as SelectType,
|
||||
selectMode: blockSelectMode,
|
||||
withinLocalizedField: fieldWithinLocalizedField,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field, SanitizedConfig } from 'payload'
|
||||
import { flattenAllFields, type Field, type SanitizedConfig } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
|
||||
@@ -74,7 +74,28 @@ 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',
|
||||
@@ -137,6 +158,11 @@ const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blockReferences',
|
||||
type: 'blocks',
|
||||
blockReferences: ['reference-block'],
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
@@ -321,6 +347,14 @@ describe('sanitizeRelationshipIDs', () => {
|
||||
group: { ...relsData },
|
||||
},
|
||||
],
|
||||
blockReferences: [
|
||||
{
|
||||
blockType: 'reference-block',
|
||||
array: [{ ...relsData }],
|
||||
group: { ...relsData },
|
||||
...relsData,
|
||||
},
|
||||
],
|
||||
group: {
|
||||
...relsData,
|
||||
array: [{ ...relsData }],
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { APIError, traverseFields } from 'payload'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import { traverseFields } from 'payload'
|
||||
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
config: SanitizedConfig
|
||||
data: Record<string, unknown>
|
||||
fields: Field[]
|
||||
parentIsLocalized?: boolean
|
||||
}
|
||||
|
||||
interface RelationObject {
|
||||
@@ -112,6 +113,7 @@ export const sanitizeRelationshipIDs = ({
|
||||
config,
|
||||
data,
|
||||
fields,
|
||||
parentIsLocalized,
|
||||
}: Args): Record<string, unknown> => {
|
||||
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
|
||||
if (!ref || typeof ref !== 'object') {
|
||||
@@ -124,7 +126,7 @@ export const sanitizeRelationshipIDs = ({
|
||||
}
|
||||
|
||||
// handle localized relationships
|
||||
if (config.localization && field.localized) {
|
||||
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
const locales = config.localization.locales
|
||||
const fieldRef = ref[field.name]
|
||||
if (typeof fieldRef !== 'object') {
|
||||
@@ -150,7 +152,14 @@ export const sanitizeRelationshipIDs = ({
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({ callback: sanitize, fields, fillEmpty: false, ref: data })
|
||||
traverseFields({
|
||||
callback: sanitize,
|
||||
config,
|
||||
fields,
|
||||
fillEmpty: false,
|
||||
parentIsLocalized,
|
||||
ref: data,
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -9,32 +9,40 @@ 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 = []
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
joins.forEach(({ condition, table }) => {
|
||||
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: useDistinct ? sql`COUNT(DISTINCT ${this.tables[tableName].id})` : count(),
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where),
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1),
|
||||
})
|
||||
|
||||
return Number(countResult[0].count)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { sql } from 'drizzle-orm'
|
||||
import { fieldIsVirtual } from 'payload/shared'
|
||||
import { and, asc, desc, eq, or, sql } from 'drizzle-orm'
|
||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js'
|
||||
@@ -10,11 +11,49 @@ 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
|
||||
@@ -26,6 +65,7 @@ type TraverseFieldArgs = {
|
||||
joinQuery: JoinQuery
|
||||
joins?: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
parentIsLocalized?: boolean
|
||||
path: string
|
||||
select?: SelectType
|
||||
selectAllOnCurrentLevel?: boolean
|
||||
@@ -34,7 +74,6 @@ type TraverseFieldArgs = {
|
||||
topLevelArgs: Record<string, unknown>
|
||||
topLevelTableName: string
|
||||
versions?: boolean
|
||||
withinLocalizedField?: boolean
|
||||
withTabledFields: {
|
||||
numbers?: boolean
|
||||
rels?: boolean
|
||||
@@ -53,6 +92,7 @@ export const traverseFields = ({
|
||||
joinQuery = {},
|
||||
joins,
|
||||
locale,
|
||||
parentIsLocalized = false,
|
||||
path,
|
||||
select,
|
||||
selectAllOnCurrentLevel = false,
|
||||
@@ -61,7 +101,6 @@ export const traverseFields = ({
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
versions,
|
||||
withinLocalizedField = false,
|
||||
withTabledFields,
|
||||
}: TraverseFieldArgs) => {
|
||||
fields.forEach((field) => {
|
||||
@@ -69,6 +108,11 @@ export const traverseFields = ({
|
||||
return
|
||||
}
|
||||
|
||||
const isFieldLocalized = fieldShouldBeLocalized({
|
||||
field,
|
||||
parentIsLocalized,
|
||||
})
|
||||
|
||||
// handle simple relationship
|
||||
if (
|
||||
depth > 0 &&
|
||||
@@ -76,7 +120,7 @@ export const traverseFields = ({
|
||||
!field.hasMany &&
|
||||
typeof field.relationTo === 'string'
|
||||
) {
|
||||
if (field.localized) {
|
||||
if (isFieldLocalized) {
|
||||
_locales.with[`${path}${field.name}`] = true
|
||||
} else {
|
||||
currentArgs.with[`${path}${field.name}`] = true
|
||||
@@ -152,13 +196,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,
|
||||
})
|
||||
|
||||
@@ -185,7 +229,8 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
const blockKey = `_blocks_${block.slug}`
|
||||
|
||||
let blockSelect: boolean | SelectType | undefined
|
||||
@@ -262,13 +307,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,
|
||||
})
|
||||
|
||||
@@ -304,6 +349,7 @@ export const traverseFields = ({
|
||||
joinQuery,
|
||||
joins,
|
||||
locale,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
path: `${path}${field.name}_`,
|
||||
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
|
||||
selectAllOnCurrentLevel:
|
||||
@@ -315,7 +361,6 @@ export const traverseFields = ({
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
versions,
|
||||
withinLocalizedField: withinLocalizedField || field.localized,
|
||||
withTabledFields,
|
||||
})
|
||||
|
||||
@@ -342,6 +387,7 @@ export const traverseFields = ({
|
||||
|
||||
const {
|
||||
limit: limitArg = field.defaultLimit ?? 10,
|
||||
page,
|
||||
sort = field.defaultSort,
|
||||
where,
|
||||
} = joinQuery[joinSchemaPath] || {}
|
||||
@@ -352,113 +398,230 @@ 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
|
||||
|
||||
for (let key in selectFields) {
|
||||
const val = selectFields[key]
|
||||
if (Array.isArray(field.collection)) {
|
||||
let currentQuery: null | SQLSelect = null
|
||||
const onPath = field.on.split('.').join('_')
|
||||
|
||||
if (val.table && getNameFromDrizzleTable(val.table) === joinCollectionTableName) {
|
||||
delete selectFields[key]
|
||||
key = key.split('.').pop()
|
||||
selectFields[key] = newAliasTable[key]
|
||||
if (Array.isArray(sort)) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
const subQuery = chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select(selectFields as any)
|
||||
.from(newAliasTable)
|
||||
.where(subQueryWhere)
|
||||
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
||||
}).as(subQueryAlias)
|
||||
let sanitizedSort = sort
|
||||
|
||||
currentArgs.extras[columnName] = sql`${db
|
||||
.select({
|
||||
result: jsonAggBuildObject(adapter, {
|
||||
id: sql.raw(`"${subQueryAlias}".id`),
|
||||
...(selectFields._locale && {
|
||||
locale: sql.raw(`"${subQueryAlias}".${selectFields._locale.name}`),
|
||||
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,
|
||||
})
|
||||
.from(sql`${subQuery}`)}`.as(subQueryAlias)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
@@ -468,7 +631,7 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
const args = field.localized ? _locales : currentArgs
|
||||
const args = isFieldLocalized ? _locales : currentArgs
|
||||
if (!args.columns) {
|
||||
args.columns = {}
|
||||
}
|
||||
@@ -530,7 +693,7 @@ export const traverseFields = ({
|
||||
if (select || selectAllOnCurrentLevel) {
|
||||
const fieldPath = `${path}${field.name}`
|
||||
|
||||
if ((field.localized || withinLocalizedField) && _locales) {
|
||||
if ((isFieldLocalized || parentIsLocalized) && _locales) {
|
||||
_locales.columns[fieldPath] = true
|
||||
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
|
||||
currentArgs.columns[fieldPath] = true
|
||||
@@ -552,7 +715,7 @@ export const traverseFields = ({
|
||||
) {
|
||||
const fieldPath = `${path}${field.name}`
|
||||
|
||||
if ((field.localized || withinLocalizedField) && _locales) {
|
||||
if ((isFieldLocalized || parentIsLocalized) && _locales) {
|
||||
_locales.columns[fieldPath] = true
|
||||
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
|
||||
currentArgs.columns[fieldPath] = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { ChainedMethods, TransactionPg } from '../types.js'
|
||||
import type { ChainedMethods } from '../types.js'
|
||||
import type { BasePostgresAdapter, CountDistinct } from './types.js'
|
||||
|
||||
import { chainMethods } from '../find/chainMethods.js'
|
||||
@@ -9,33 +9,40 @@ 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 = []
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
joins.forEach(({ condition, table }) => {
|
||||
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 as TransactionPg)
|
||||
query: db
|
||||
.select({
|
||||
count: useDistinct ? sql`COUNT(DISTINCT ${this.tables[tableName].id})` : count(),
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where),
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1),
|
||||
})
|
||||
|
||||
return Number(countResult[0].count)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user