# Fixing invalid html from convertLexicalToHTML with UploadConverter
## What?
When using the [convertLexicalToHTML
function](https://github.com/payloadcms/payload/blob/main/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/index.ts#L33C17-L33C37)
to convert richtext into html, the resulting html is incorrect when an
upload is present.
## Why?
The error is within the UploadHTMLConverter, as you can see in my
commits.
The picture closing tag has a Dollar sign in it `</picture$>`, which is
invalid html. This results in the picture not closing at the right
place. The picture element is not only wrapping the img tag, but also
all following content of that richtext field.
> I suppose that dollar sign ended up there due to auto rename tags from
IDE.
```ts
import { convertLexicalToHTML } from "@payloadcms/richtext-lexical/html";
const html = convertLexicalToHTML({ data: content });
```
**Example before fix**
```html
<div class="payload-richtext">
<h2>Title</h2>
<p>description</p>
<picture>
<img alt="media.png" height="400" src="https://some.url/storage/v1/object/public/media/media.png" width="684">
<p>Some more text</p>
</picture>
</div>
```
**Example after fix**
```html
<div class="payload-richtext">
<h2>Title</h2>
<p>description</p>
<picture>
<img alt="media.png" height="400" src="https://some.url/storage/v1/object/public/media/media.png" width="684">
</picture>
<p>Some more text</p>
</div>
```
This PR fixes two bugs in the version diff view SetStepNav component
### Bug 1: Document title isn't shown correctly in the step navigation
if the field of `useAsTitle` is nested inside a presentational field.
The StepNav shows the title of the document consistently throughout
every view except the version diff view. In the version diff view, the
document title is always `[Untitled]` if the field of `useAsTitle` is
nested inside presentational fields. Below is a video demo of the bug:
https://github.com/user-attachments/assets/23cb140a-b6d3-4d39-babf-5e4878651869
This happens because the fields of the collection/global aren't
flattened inside SetStepNav and thus the component is not accessing the
field data correctly. This results in the title being `null` causing the
fallback title to be shown.
### Bug 2: Step navigation shows the title of the version viewed, not
the current version
The StepNav component takes the title of the current version viewed.
This causes the second part of the navigation path to change between
versions which is inconsistent between other views and doesn't seem
intentional, although it could be. Below is a video of the bug with the
first bug fixed by flattening the fields:
https://github.com/user-attachments/assets/e5beb9b3-8e2e-4232-b1e5-5cce720e46b9
This happens due to the fact that the title is taken from the
`useAsTitle` field of the **viewed** version rather than the **current**
version. This bug is fixed by using the `useDocumentTitle` hook from the
ui package instead of passing the version's `useAsTitle` data down the
component tree. The final state of the step navigation is shown in the
following video:
https://github.com/user-attachments/assets/a69d5088-e7ee-43be-8f47-d9775d43dde9
I also added a test to test that the title part in the step navigation
stays consistent between versions and implicitly also tests that the
document title is shown correctly in the step nav if the field of
`useAsTitle` is a nested inside a presentational field.
Root level collection folders should not display items, only folders.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210821658736793
Adds the `admin.autoRefresh` property to the root config. This allows
users to stay logged and have their token always refresh in the
background without being prompted with the "Stay Logged In?" modal.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211114366468735
Previously, we were sending incorrect API requests for globals.
Additionally, the global findOne endpoint did not respect the data
attribute.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211249618316704
### What?
Set X-Payload-HTTP-Method-Override as allowed cross origin header
### Why?
As of #13619 the live preview uses POST method to retrieve updated data.
When running cms and website on different origins, the cross origin
requires X-Payload-HTTP-Method-Override header to be allowed
### How?
Adding X-Payload-HTTP-Method-Override as a default allowed header
This release upgrades the lexical dependency from 0.34.0 to 0.35.0.
If you installed lexical manually, update it to 0.35.0. Installing
lexical manually is not recommended, as it may break between updates,
and our re-exported versions should be used. See the [yellow banner
box](https://payloadcms.com/docs/rich-text/custom-features) for details.
If you still encounter richtext-lexical errors, do the following, in
this order:
- Delete node_modules
- Delete your lockfile (e.g. pnpm-lock.json)
- Reinstall your dependencies (e.g. pnpm install)
### Lexical Breaking Changes
The following Lexical releases describe breaking changes. We recommend
reading them if you're using Lexical APIs directly
(`@payloadcms/richtext-lexical/lexical/*`).
- [v.0.35.0](https://github.com/facebook/lexical/releases/tag/v0.35.0)
Alternative solution to
https://github.com/payloadcms/payload/pull/11104. Big thanks to
@andershermansen and @GermanJablo for kickstarting work on a solution
and bringing this to our attention. This PR copies over the live-preview
test suite example from his PR.
Fixes https://github.com/payloadcms/payload/issues/5285,
https://github.com/payloadcms/payload/issues/6071 and
https://github.com/payloadcms/payload/issues/8277. Potentially fixes
#11801
This PR completely gets rid of our client-side live preview field
traversal + population and all logic related to it, and instead lets the
findByID endpoint handle it.
The data sent through the live preview message event is now passed to
findByID via the newly added `data` attribute. The findByID endpoint
will then use this data and run hooks on it (which run population),
instead of fetching the data from the database.
This new API basically behaves like a `/api/populate?data=` endpoint,
with the benefit that it runs all the hooks. Another use-case for it
will be rendering lexical data. Sometimes you may only have unpopulated
data available. This functionality allows you to then populate the
lexical portion of it on-the-fly, so that you can properly render it to
JSX while displaying images.
## Benefits
- a lot less code to maintain. No duplicative population logic
- much faster - one single API request instead of one request per
relationship to populate
- all payload features are now correctly supported (population and
hooks)
- since hooks are now running for client-side live preview, this means
the `lexicalHTML` field is now supported! This was a long-running issue
- this fixes a lot of population inconsistencies that we previously did
not know of. For example, it previously populated lexical and slate
relationships even if the data was saved in an incorrect format
## [Method Override
(POST)](https://payloadcms.com/docs/rest-api/overview#using-method-override-post)
change
The population request to the findByID endpoint is sent as a post
request, so that we can pass through the `data` without having to
squeeze it into the url params. To do that, it uses the
`X-Payload-HTTP-Method-Override` header.
Previously, this functionality still expected the data to be sent
through as URL search params - just passed to the body instead of the
URL. In this PR, I made it possible to pass it as JSON instead. This
means:
- the receiving endpoint will receive the data under `req.data` and is
not able to read it from the search params
- this means existing endpoints won't support this functionality unless
they also attempt to read from req.data.
- for the purpose of this PR, the findByID endpoint was modified to
support this behavior. This functionality is documented as it can be
useful for user-defined endpoints as well.
Passing data as json has the following benefits:
- it's more performant - no need to serialize and deserialize data to
search params via `qs-esm`. This is especially important here, as we are
passing large amounts of json data
- the current implementation was serializing the data incorrectly,
leading to incorrect data within nested lexical nodes
**Note for people passing their own live preview `requestHandler`:**
instead of sending a GET request to populate documents, you will now
need to send a POST request to the findByID endpoint and pass additional
headers. Additionally, you will need to send through the arguments as
JSON instead of search params and include `data` as an argument. Here is
the updated defaultRequestHandler for reference:
```ts
const defaultRequestHandler: CollectionPopulationRequestHandler = ({
apiPath,
data,
endpoint,
serverURL,
}) => {
const url = `${serverURL}${apiPath}/${endpoint}`
return fetch(url, {
body: JSON.stringify(data),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Payload-HTTP-Method-Override': 'GET',
},
method: 'POST',
})
}
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211124793355068
- https://app.asana.com/0/0/1211124793355066
### What?
Encode the cacheTag in the ImageMedia component
### Why?
In the website template, media is render using the updatedAt field as a
cacheTag, this value causes an InvalidQueryStringException when
deploying to Cloudfront
### How?
Uses encodeURIComponent on encode the date value of updatedAt
Fixes https://github.com/payloadcms/payload/issues/13557
### What?
- Fixed an issue where virtual relationship fields (`virtual: string`,
e.g. `post.title`) could not be used in the WhereBuilder filter
dropdown.
- Ensured regular virtual fields (`virtual: true`) are still excluded
since they are not backed by database fields.
### Why?
Previously, attempting to filter by a virtual relationship caused
runtime errors because the field was treated as a plain text field. At
the same time, non-queryable virtuals needed to remain excluded to avoid
invalid queries.
### How?
- Updated `reduceFieldsToOptions` to recognize `virtual: string` fields
and map them to their underlying path while keeping their declared type
and operators.
- Continued to filter out `virtual: true` fields in the same guard used
for hidden/disabled fields.
### What?
In the create-first-user view, fields like `richText` were being marked
as `readOnly: true` because they had no permissions entry in the
permissions map.
### Why?
The view was passing an incomplete `docPermissions` object.
When a field had no entry in `docPermissions.fields`, `renderField`
received `permissions: undefined`, which was interpreted as denied
access.
This caused fields (notably `richText`) to default to read-only even
though the user should have full access when creating the first user.
### How?
- Updated the create-first-user view to always pass a complete
`docPermissions` object.
- Default all fields in the user collection to `{ create: true, read:
true, update: true }`.
- Ensures every field is explicitly granted full access during the
first-user flow.
- Keeps the `renderField` logic unchanged and aligned with Payload’s
permission model.
Fixes#13612
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211211792037939
Edited by @jacobsfletch
Autosave is run with default depth, causing form state and the document
info context to be inconsistent across the load/autosave/save lifecycle.
For example, when loading the app, the document is queried with `depth:
0` and relationships are _not_ populated. Same with save/draft/publish.
Autosave, on the other hand, populates these relationships, causing the
data to temporarily change in shape in between these events.
Here's an example:
https://github.com/user-attachments/assets/153ea112-7591-4f54-9216-575322f4edbe
Now, autosave runs with `depth: 0` as expected, consistent with the
other events of the document lifecycle.
**Plus this is a huge performance improvement.**
Fixes#13643 and #13192.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211224908421570
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
### What?
This PR updates the folder configuration types to explicitly omit the
`trash` option from `CollectionConfig` when used for folders.
### Why?
Currently, only documents are designed to support trashing. Allowing
folders to be trashed introduces inconsistencies, since removing a
folder does not properly remove its references from associated
documents.
By omitting `trash` from folder configurations, we prevent accidental
use of unsupported behavior until proper design and handling for folder
trashing is implemented.
### How?
- Updated `RootFoldersConfiguration.collectionOverrides` type to use
`Omit<CollectionConfig, 'trash'>`
- Ensures `trash` cannot be enabled on folders
### What?
Adds a new `experimental.localizeStatus` config option, set to `false`
by default. When `true`, the admin panel will display the document
status based on the *current locale* instead of the _latest_ overall
status. Also updates the edit view to only show a `changed` status when
`autosave` is enabled.
### Why?
Showing the status for the current locale is more accurate and useful in
multi-locale setups. This update will become default behavior, able to
be opted in by setting `experimental.localizeStatus: true` in the
Payload config. This option will become depreciated in V4.
### How?
When `localizeStatus` is `true`, we store the localized status in a new
`localeStatus` field group within version data. The admin panel then
reads from this field to display the correct status for the current
locale.
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Fixes https://github.com/payloadcms/payload/issues/10379
During form state requests, the passed `data` did not have access to the
document ID. This was because the data we use came from the client,
passed as an argument. The client did not pass data that included the
document ID.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211203844178567
### What?
Adds comprehensive documentation for the RadioField component in the
form builder plugin documentation.
### Why?
Addresses review feedback from #11908 noting that the RadioField was not
documented after being exported for end-user use.
### How?
- Added RadioField section with complete property documentation
- Added detailed Radio Options sub-section explaining option structure
- Updated Select field documentation for consistency (added missing
placeholder property and detailed options structure)
- Added radio field to configuration example to show all available
fields
Fixes the documentation gap identified in #11908 review comments.
Fixes
https://github.com/payloadcms/payload/issues/13589#issuecomment-3234832478
### Problem
The baseFilter is being applied when a user has no tenant selected, is
assigned to tenants BUT also passes the `userHasAccessToAllTenants`
check.
### Fix
Do not apply the baseFilter when no tenant is selected and the user
passes the `userHasAccessToAllTenants` check.
### What?
Restores the ability to query by `Date(...)` when using the `sqlite`
adapter.
### Why?
Queries involving JavaScript `Date`s return incorrect results because
they are not converted to ISO 8601 strings by the `sqlite` adapter.
### How?
Wraps comparison operators to convert `Date` parameters to ISO 8601
strings.
Fixes#11692
---------
Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
### What?
Hides the `Publish in [specific locale]` button on collections and
globals when no localized fields are present.
### Why?
Currently, the `Publish in [specific locale]` shows on every
collection/global as long as `localization` is enabled in the Payload
config, however this is not necessary when the collection/global has no
localized fields.
### How?
Checks that localized fields exist prior to rendering the `Publish in
[specific locale]` button.
Fixes#13447
---------
Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
### What?
Currently the `DraggableBlockPlugin` and `AddBlockHandlePlugin`
components are automatically applied to every editor. For flexibility
purposes, we want to allow these to be optionally removed when needed.
### Why?
There are scenarios where you may want to enforce certain limitations on
an editor, such as only allowing a single line of text. The draggable
block element and add block button wouldn't play nicely with this
scenario.
Previously in order to do this, you needed to use custom css to hide the
elements, which still technically allows them to be accessible to the
end-user if they removed the CSS. This implementation ensures the
handlers are properly removed when not wanted.
### How?
Add `hideDraggableBlockElement` and `hideAddBlockButton` options to the
lexical `admin` property. When these are set to `true`, the
`DraggableBlockPlugin` and `AddBlockHandlePlugin` are not rendered to
the DOM.
Addresses #13636
Reindexing all collections in the search plugin was previously done in
sequence which is slow and risks timing out under certain network
conditions. By running these requests in parallel we are able to save
**on average ~80%** in compute time.
This test includes reindexing 2000 documents in total across 2
collections, 3 times over. The indexes themselves are relatively simple,
containing only a couple simple fields each, so the savings would only
increase with more complexity and/or more documents.
Before:
Attempt 1: 38434.87ms
Attempt 2: 47852.61ms
Attempt 3: 28407.79ms
Avg: 38231.75ms
After:
Attempt 1: 7834.29ms
Attempt 2: 7744.40ms
Attempt 3: 7918.58ms
Avg: 7832.42ms
Total savings: ~79.51%
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211162504205343
### What?
Fix `JSON` field so that it respects `admin.editorOptions` (e.g.
`tabSize`, `insertSpaces`, etc.), matching the behavior of the `code`
field. Also refactor `CodeEditor` to set indentation and whitespace
options per-model instead of globally.
### Why?
- Previously, the JSON field ignored `editorOptions` and always
serialized with spaces (`tabSize: 2`). This caused inconsistencies when
comparing JSON and code fields configured with the same options.
- Monaco’s global defaults were being overridden in a way that leaked
settings between editors, making per-field customization unreliable.
### How?
- Updated `JSON` field to extract `tabSize` from `editorOptions` and
pass it through consistently when serializing and mounting the editor.
- Refactored CodeEditor to:
- Disable `detectIndentation` globally.
- Apply `insertSpaces`, `tabSize`, and `trimAutoWhitespace` on a
per-model basis inside onMount.
- Preserve all other `editorOptions` as before.
Fixes#13583
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211177100283503
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Fixes https://github.com/payloadcms/payload/issues/13563
When using the nested docs plugin with collections that do not have
drafts enabled. It was not syncing breadcrumbs from parent changes.
The root cause was returning early on any document that did not meet
`doc._status !== 'published'` check, which was **_always_** the case for
non-draft collections.
### Before
```ts
if (doc._status !== 'published') {
return
}
```
### After
```ts
if (collection.versions.drafts && doc._status !== 'published') {
return
}
```
Setting `runHooks: true` is already discouraged and will make the job
queue system a lot slower and less reliable. We have no test coverage
for this and it's additional code we need to maintain.
To further discourage using this property, this PR marks it as
deprecated and for removal in 4.0.
Currently, attempting to run tasks in parallel will result in DB errors.
## Solution
The problem was caused due to inefficient db update calls. After each
task completes, we need to update the log array in the payload-jobs
collection. On postgres, that's a different table.
Currently, the update works the following way:
1. Nuke the table
2. Re-insert every single row, including the new one
This will throw db errors if multiple processes start doing that.
Additionally, due to conflicts, new log rows may be lost.
This PR makes use of the the [new db $push operation
](https://github.com/payloadcms/payload/pull/13453) we recently added to
atomically push a new log row to the database in a single round-trip.
This not only reduces the amount of db round trips (=> faster job queue
system) but allows multiple tasks to perform this db operation in
parallel, without conflicts.
## Problem
**Example:**
```ts
export const fastParallelTaskWorkflow: WorkflowConfig<'fastParallelTask'> = {
slug: 'fastParallelTask',
handler: async ({nlineTask }) => {
const taskFunctions = []
for (let i = 0; i < 20; i++) {
const idx = i + 1
taskFunctions.push(async () => {
return await inlineTask(`parallel task ${idx}`, {
input: {
test: idx,
},
task: () => {
return {
output: {
taskID: idx.toString(),
},
}
},
})
})
}
await Promise.all(taskFunctions.map((f) => f()))
},
}
```
On SQLite, this would throw the following error:
```bash
Caught error Error: UNIQUE constraint failed: payload_jobs_log.id
at Object.next (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/libsql@0.4.7/node_modules/libsql/index.js:335:20)
at Statement.all (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/libsql@0.4.7/node_modules/libsql/index.js:360:16)
at executeStmt (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5/node_modules/@libsql/client/lib-cjs/sqlite3.js:285:34)
at Sqlite3Client.execute (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5/node_modules/@libsql/client/lib-cjs/sqlite3.js:101:16)
at /Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/libsql/session.ts:288:58
at LibSQLPreparedQuery.queryWithCache (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/sqlite-core/session.ts:79:18)
at LibSQLPreparedQuery.values (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/libsql/session.ts:286:21)
at LibSQLPreparedQuery.all (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/libsql/session.ts:214:27)
at QueryPromise.all (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/sqlite-core/query-builders/insert.ts:402:26)
at QueryPromise.execute (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/sqlite-core/query-builders/insert.ts:414:40)
at QueryPromise.then (/Users/alessio/Documents/GitHub/payload/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/query-promise.ts:31:15) {
rawCode: 1555,
code: 'SQLITE_CONSTRAINT_PRIMARYKEY',
libsqlError: true
}
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211001438499053
Conditionally send the user to the inactivity route if there was a user
but refresh failed. You can get into an infinite loop if you call this
function externally and a redirect is being used in the url.
Follow up to #12119.
You can now configure the toast notifications used in the admin panel
through the Payload config:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// ...
toast: {
duration: 8000,
limit: 1,
// ...
}
}
})
```
_Note: the toast config is temporarily labeled as experimental to allow
for changes to the API, if necessary._
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211139422639711
This PR adds **atomic** `$push` **support for array fields**. It makes
it possible to safely append new items to arrays, which is especially
useful when running tasks in parallel (like job queues) where multiple
processes might update the same record at the same time. By handling
pushes atomically, we avoid race conditions and keep data consistent -
especially on postgres, where the current implementation would nuke the
entire array table before re-inserting every single array item.
The feature works for both localized and unlocalized arrays, and
supports pushing either single or multiple items at once.
This PR is a requirement for reliably running parallel tasks in the job
queue - see https://github.com/payloadcms/payload/pull/13452.
Alongside documenting `$push`, this PR also adds documentation for
`$inc`.
## Changes to updatedAt behavior
https://github.com/payloadcms/payload/pull/13335 allows us to override
the updatedAt property instead of the db always setting it to the
current date.
However, we are not able to skip updating the updatedAt property
completely. This means, usage of $push results in 2 postgres db calls:
1. set updatedAt in main row
2. append array row in arrays table
This PR changes the behavior to only automatically set updatedAt if it's
undefined. If you explicitly set it to `null`, this now allows you to
skip the db adapter automatically setting updatedAt.
=> This allows us to use $push in just one single db call
## Usage Examples
### Pushing a single item to an array
```ts
const post = (await payload.db.updateOne({
data: {
array: {
$push: {
text: 'some text 2',
id: new mongoose.Types.ObjectId().toHexString(),
},
},
},
collection: 'posts',
id: post.id,
}))
```
### Pushing a single item to a localized array
```ts
const post = (await payload.db.updateOne({
data: {
arrayLocalized: {
$push: {
en: {
text: 'some text 2',
id: new mongoose.Types.ObjectId().toHexString(),
},
es: {
text: 'some text 2 es',
id: new mongoose.Types.ObjectId().toHexString(),
},
},
},
},
collection: 'posts',
id: post.id,
}))
```
### Pushing multiple items to an array
```ts
const post = (await payload.db.updateOne({
data: {
array: {
$push: [
{
text: 'some text 2',
id: new mongoose.Types.ObjectId().toHexString(),
},
{
text: 'some text 3',
id: new mongoose.Types.ObjectId().toHexString(),
},
],
},
},
collection: 'posts',
id: post.id,
}))
```
### Pushing multiple items to a localized array
```ts
const post = (await payload.db.updateOne({
data: {
arrayLocalized: {
$push: {
en: {
text: 'some text 2',
id: new mongoose.Types.ObjectId().toHexString(),
},
es: [
{
text: 'some text 2 es',
id: new mongoose.Types.ObjectId().toHexString(),
},
{
text: 'some text 3 es',
id: new mongoose.Types.ObjectId().toHexString(),
},
],
},
},
},
collection: 'posts',
id: post.id,
}))
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211110462564647
### What
When using the `Restore as draft` action from the version view, the
restored document incorrectly appears as `published` in the API. This
issue was reported by a client.
### Why
In the `restoreVersion` operation, the document is updated via
`db.updateOne` (line 265). The update passes along the status stored in
the version being restored but does not respect the `draft` query
parameter.
### How
Ensures that the result status is explicitly set to `draft` when the
`draft` argument is `true`.
Fixes https://github.com/payloadcms/payload/issues/13433. Testing
release: `3.54.0-internal.90cf7d5`
Previously, when calling `getPayload`, you would always use the same,
cached payload instance within a single process, regardless of the
arguments passed to the `getPayload` function. This resulted in the
following issues - both are fixed by this PR:
- If, in your frontend you're calling `getPayload` without `cron: true`,
and you're hosting the Payload Admin Panel in the same process, crons
will not be enabled even if you visit the admin panel which calls
`getPayload` with `cron: true`. This will break jobs autorun depending
on which page you visit first - admin panel or frontend
- Within the same process, you are unable to use `getPayload` twice for
different instances of payload with different Payload Configs.
On postgres, you can get around this by manually calling new
`BasePayload()` which skips the cache. This did not work on mongoose
though, as mongoose was caching the models on a global singleton (this
PR addresses this).
In order to bust the cache for different Payload Config, this PR
introduces a new, optional `key` property to `getPayload`.
## Mongoose - disable using global singleton
This PR refactors the Payload Mongoose adapter to stop relying on the
global mongoose singleton. Instead, each adapter instance now creates
and manages its own scoped Connection object.
### Motivation
Previously, calling `getPayload()` more than once in the same process
would throw `Cannot overwrite model` errors because models were compiled
into the global singleton. This prevented running multiple Payload
instances side-by-side, even when pointing at different databases.
### Changes
- Replace usage of `mongoose.connect()` / `mongoose.model()` with
instance-scoped `createConnection()` and `connection.model()`.
- Ensure models, globals, and versions are compiled per connection, not
globally.
- Added proper `close()` handling on `this.connection` instead of
`mongoose.disconnect()`.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211114366468745
### What?
Brackets (`[ ]`) in option values end up in GraphQL enum names via
`formatName`, causing schema generation to fail. This PR adds a single
rule to `formatName`:
- replace `[` and `]` with `_`
### Why?
Using `_` (instead of removing the brackets) is safer and more
consistent:
- Avoid collisions: removal can merge distinct strings (`"A[B]"` →
`"AB"`). `_` keeps them distinct (`"A_B"`).
- **Consistency**: `formatName` already maps punctuation to `_` (`. - /
+ , ( ) '`). Brackets follow the same rule.
Readability: `mb-[150px]` → `mb__150px_` is clearer than `mb150px`.
Digits/units safety: removal can jam characters (`w-[2/3]` → `w23`); `_`
avoids that (`w_2_3_`).
### How?
Update formatName to include a bracket replacement step:
```
.replace(/\[|\]/g, '_')
```
No other call sites or value semantics change; only names containing
brackets are affected.
Fixes#13466
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211141396953194
Fixes#13574.
When editing an autosave-enabled document within a document drawer, any
changes made will while the autosave is processing are ultimately
discarded from local form state. This makes is difficult or even
impossible to edit fields.
This is because we server-render, then replace, the entire document on
every save. This includes form state, which is stale because it was
rendered while new changes were still being made. We don't need to
re-render the entire view on every save, though, only on create. We
don't do this on the top-level edit view, for example. Instead, we only
need to replace form state.
This change is also a performance improvement because we are no longer
rendering all components unnecessarily, especially on every autosave
interval.
Before:
https://github.com/user-attachments/assets/e9c221bf-4800-4153-af55-8b82e93b3c26
After:
https://github.com/user-attachments/assets/d77ef2f3-b98b-41d6-ba6c-b502b9bb99cc
Note: ignore the flashing autosave status and doc controls. This is
horrible and we're actively fixing it, but is outside the scope of this
PR.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211139422639700
Fixes https://github.com/payloadcms/payload/issues/13565
The logout operation was running twice and causing a race condition on
user updates. This change ensures the logout operation only runs 1 time.
Really this view should have 1 purpose and that is to show the
inactivity view. Currently it has 2 purposes which is why it needs the
useEffect — the root of this issue. Instead we should just call the
`logOut` function from the logout button instead of it linking to a
logout page.
### What?
Ensure the Rich Text field is read-only when viewing a locked document.
### Why?
When opening a locked doc in read-only mode, the Rich Text field could
still appear editable. This is inconsistent with other fields and allows
users to try typing into a locked document.
### How?
- Pass `readOnly={isTrashedDoc || isLocked}` into `buildFormState` in
the edit view renderer.
Fixes#13559
Re-enable the global tenant selector on all views. In the last release
the global tenant filter was only enabled on tenant enabled collection
list views. This change allows the global tenant filter to be selected
on all non-document views. This is useful on custom views or custom
components on views that may not be tenant-enabled.
The jobs stats global is used internally to get the scheduling system to
work. Currently, if scheduling is enabled, the "Payload Jobs Stats"
global is visible in the Payload Admin Panel:
<img width="524" height="252" alt="Screenshot 2025-08-23 at 00 10 13@2x"
src="https://github.com/user-attachments/assets/91702c93-4b58-4990-922b-8241ea9aa00e"
/>
Similarly to how we do it in the Payload Jobs collection, this PR hides
the payload jobs stats global from the admin panel by default
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211124797199081
### What?
Prevents the Auth component from rendering an empty `.auth-fields`
wrapper.
### Why?
When `disableLocalStrategy` is true and `enableFields` is false, but
`useAPIKey` is true while
read access to API key fields is denied, the component still rendered
the parent wrapper with a
background—showing a blank box.
### How?
Introduce `hasVisibleContent`:
- `showAuthBlock = enableFields`
- `showAPIKeyBlock = useAPIKey && canReadApiKey`
- `showVerifyBlock = verify && isEditing`
If none are true, return `null`. (`disableLocalStrategy` is already
accounted for via `enableFields`.)
Fixes#12089
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211117270523574
Continuation of https://github.com/payloadcms/payload/pull/13501.
When merging server form state with `acceptValues: true`, like on submit
(not autosave), rows are not deeply merged causing custom row
components, like row labels, to disappear. This is because we never
attach components to the form state response unless it has re-rendered
server-side, so unless we merge these rows with the current state, we
lose them.
Instead of allowing `acceptValues` to override all local changes to
rows, we need to flag any newly added rows with `addedByServer` so they
can bypass the merge strategy. Existing rows would continue to be merged
as expected, and new rows are simply appended to the end.
Discovered here:
https://discord.com/channels/967097582721572934/967097582721572937/1408367321797365840
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211115023863814
Previously, when manually setting `createdAt` or `updatedAt` in a
`payload.db.*` or `payload.*` operation, the value may have been
ignored. In some cases it was _impossible_ to change the `updatedAt`
value, even when using direct db adapter calls. On top of that, this
behavior sometimes differed between db adapters. For example, mongodb
did accept `updatedAt` when calling `payload.db.updateVersion` -
postgres ignored it.
This PR changes this behavior to consistently respect `createdAt` and
`updatedAt` values for `payload.db.*` operations.
For `payload.*` operations, this also works with the following
exception:
- update operations do no respect `updatedAt`, as updates are commonly
performed by spreading the old data, e.g. `payload.update({ data:
{...oldData} })` - in these cases, we usually still want the `updatedAt`
to be updated. If you need to get around this, you can use the
`payload.db.updateOne` operation instead.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210919646303994
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
-->
### What?
Fixes a tiny typo in the documentation.
<!--
### Why?
### How?
Fixes #
-->
Co-authored-by: lstellway <lstellway@users.noreply.github.com>
Fixes#10526
### What?
`updateVersion` throws error at deleting before version of draft.
### Why?
When triggers `db.updateVersion` with non-mongodb environment,
`saveVersion` in `payload/src/versions/saveVersions.ts` doesn't remove
`id` in `versionData`.
### How?
Add `delete versionData.id` for non-mongodb environments.
---------
Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
Fixes#13527.
When upgrading to [Next.js 15.5](https://nextjs.org/blog/next-15-5) with
Payload, you might experience a runtime or build error similar to this:
```ts
Type error: Type 'typeof import("/src/app/(payload)/api/graphql/route")' does not satisfy the expected type 'RouteHandlerConfig<"/api/graphql">'.
Types of property 'OPTIONS' are incompatible.
Type '(request: Request, args: { params: Promise<{ slug: string[]; }>; }) => Promise<Response>' is not assignable to type '(request: NextRequest, context: { params: Promise<{}>; }) => void | Promise<void> | Response | Promise<Response>'.
Types of parameters 'args' and 'context' are incompatible.
Type '{ params: Promise<{}>; }' is not assignable to type '{ params: Promise<{ slug: string[]; }>; }'.
Types of property 'params' are incompatible.
Type 'Promise<{}>' is not assignable to type 'Promise<{ slug: string[]; }>'.
Property 'slug' is missing in type '{}' but required in type '{ slug: string[]; }'.
```
This is because Next.js route types are now _stricter_. Our REST handler
is nested within a catch-all `/api/[...slug]` route, so the slug param
_will_ exist in the handler—but the _same_ handler is re-used for the
`/api/graphql` OPTIONS route, which **_is not_** nested within the
`slug` param and so it **_will not_** exist as the types suggest.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211115021865680
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
🤖 Automated bump of templates for v3.53.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Adjustment to https://github.com/payloadcms/payload/pull/13526
Prefer to thread ID through arguments instead of relying on routeParams
which would mean that ID is always a string - would not work for PG dbs
or non-text based ID's.
### What?
Add Icelandic translations
### Why?
It hadn't been implemented yet
### How?
I added files, mimicking the existing pattern for translations and
translated all messages in the list
### What?
Make the document `id` available to server-rendered admin components
that expect it—specifically `EditMenuItems` and
`BeforeDocumentControls`—so `props.id` matches the official docs.
### Why?
The docs show examples using `props.id`, but the runtime `serverProps`
and TS types didn’t include it. This led to `undefined` at render time.
### How?
- Add id to ServerProps and set it in renderDocumentSlots from
req.routeParams.id.
Fixes#13420
This PR fixes some incorrect field paths handling (=> should not pass
path and schema paths that contain index paths down to sub-fields
outside of the indexPath property) when building the version fields.
Fixes https://github.com/payloadcms/payload/issues/12376
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210932060696925
### What?
Remove `versionCreatedOn` from translations.
### Why?
The key is no longer referenced anywhere in the admin/UI code.
Fixes#13188
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211105332940662
### What?
Stream S3 object directly to response instead of creating a Buffer in
memory and wire up an abort controller to stop streaming if user aborts
download
### Why?
To avoid excessive memory usage and to abort s3 download if user has
aborted the request anyway.
### How?
In node environment the AWS S3 always returns a Readable. The
streamToBuffer method always required this, but the any type hided that
this was actually needed. Now there is an explicit type check, but this
should never trigger in a node server environment.
Wire up and abort controller to the request so that we tell the S3
object to also stop streaming further if the user aborts.
Fixes#10286
Maybe also helps on other issues with s3 and resource usage
No need to re-fetch doc permissions during autosave. This will save us
from making two additional client-side requests on every autosave
interval, on top of the two existing requests needed to autosave and
refresh form state.
This _does_ mean that the UI will not fully reflect permissions again
until you fully save, or until you navigating back, but that has always
been the behavior anyway (until #13416). Maybe we can find another
solution for this in the future, or otherwise consider this to be
expected behavior.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211094073049052
If you have a beforeChange hook that manipulates arrays or blocks by
_adding rows_, the result of that hook will not be reflected in the UI
after save or autosave as you might expect.
For example, this hook that ensures at least one array row is populated:
```ts
{
type: 'array',
hooks: {
beforeChange: [
({ value }) =>
!value?.length
? [
// this is an added/computed row if attempt to save with no rows
]
: value,
],
},
// ...
}
```
When you save without any rows, this hook will have automatically
computed a row for you and saved it to the database. Form state will not
reflect this fact, however, until you refresh or navigate back.
This is for two reasons:
1. When merging server form state, we receive the new fields, but do not
receive the new rows. This is because the `acceptValues` flag only
applies to the `value` property of fields, but should also apply to the
`rows` property on `array` and `blocks` fields too.
2. When creating new form state on the server, the newly added rows are
not being flagged with `addedByServer`, and so never make it into form
state when it is merged in on the client. To do this we need to send the
previous form state to the server and set `renderAllFields` to false in
order receive this property as expected. Fixed by #13524.
Before:
https://github.com/user-attachments/assets/3ab07ef5-3afd-456f-a9a8-737909b75016
After:
https://github.com/user-attachments/assets/27ad1d83-9313-45a9-b44a-db1e64452a99
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211094073049042
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
[#11716](https://github.com/payloadcms/payload/pull/11716) introduced
the `RadioField` component. This PR exports the type for use by
end-users.
### Why?
To allow end-users to utilize the type as expected with
`plugin-form-builder`.
### How?
Adds the `RadioField` interface to the list of exported plugin types.
Fixes#11806
Needed for #13501.
No need to re-render all fields during save, and especially autosave.
Fields are already rendered. We only need to render new fields that may
have been created by hooks, etc.
We can achieve this by sending previous form state and new data through
the request with `renderAllFields: false`. That way form state can be
built up from previous form state, while still accepting new data.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211094406108904
### What?
The PR #12763 added significant improvements for third-party databases
that are compatible with the MongoDB API. While the original PR was
focused on Firestore, other databases like DocumentDB also benefit from
these compatibility features.
In particular, the aggregate JOIN strategy does not work on AWS
DocumentDB and thus needs to be disabled. The current PR aims to provide
this as a sensible default in the `compatibilityOptions` that are
provided by Payload out-of-the-box.
As a bonus, it also fixes a small typo from `compat(a)bility` to
`compat(i)bility`.
### Why?
Because our Payload instance, which is backed by AWS DocumentDB, crashes
upon trying to access any `join` field.
### How?
By adding the existing `useJoinAggregations` with value `false` to the
compatiblity layer. Individual developers can still choose to override
it in their own local config as needed.
### What?
Prevent a `TypeError: Cannot read properties of null (reading
'collection')` in the admin UI when switching locales by hardening
`AuthProvider.logOut`.
### Why?
During locale transitions, user can briefly be null. The existing code
used `user.collection` unguarded
### How?
- Use `userSlug` over `user.collection`.
- Always clear local auth in a `finally` block (`setNewUser(null)`,
`revokeTokenAndExpire()`), regardless of request outcome.
Fixes#13313
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211093549155962
Completes https://github.com/payloadcms/payload/pull/13513
That PR fixed it for the `admin` suite, but I had also encountered the
same issue in `live-preview`.
When searching for instances, I found others with the same pattern
within packages, which I took the opportunity to fix in case the same
error occurs in the future.
Adds various helpers to make it easier and more standard to manage array
fields within e2e tests. Retrofits existing tests to ensure consistent
interactions across the board, and also organizes existing blocks and
relationship field helpers in the same way.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211094073049046
### What?
Update `SelectFieldBaseClientProps` type so `value` accepts `string[]`
for `hasMany` selects
### Why?
Multi-selects currently error with “Type 'string[]' is not assignable to
type 'string'”.
### How?
- Change `value?: string` to `value?: string | string[]`
- Also adds additional multi select custom component to `admin` test
suite for testing
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
### What?
Ensure the Gravatar URL appends the query string only once.
### Why?
Previously `src` used `...?${query}` while `query` already began with
`?`, producing `??` and causing the avatar URL to be invalid in some
cases.
### How?
- Keep `query` as `?${params}` (from `URLSearchParams`).
- Change `src` from `https://www.gravatar.com/avatar/${hash}?${query}`
to `https://www.gravatar.com/avatar/${hash}${query}` so only one `?` is
present.
Fixes#13325
When using plugin template , initial package.json settings is wrong.
They point to non-existing files in exports folder ( index.ts ,
index.js, index.d.ts ) , which results in broken package if published
(can't import your plugin from package)
Fixes#13426
Closes#13464
Adds a note to the Indexes docs for localized fields:
- Indexing a `localized: true` field creates one index per locale path
(e.g. `slug.en`, `slug.da-dk`), which can grow index count on MongoDB.
- Recommends defining explicit indexes via collection-level `indexes`
for only the locale paths you actually query.
- Includes a concrete example (index `slug.en` only).
Docs-only change.
---------
Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
### What?
This PR makes `filterFields` recurse into **fields with subfields**
(e.g., tabs, row, group, collapsible, array) so nested fields with
`admin.disableListColumn: true` (or hidden/disabled fields) are properly
excluded.
### Why?
Nested fields with `admin.disableListColumn: true` were still appearing
in the list view.
Example: a text field inside a `row` or `group` continued to show as a
column despite being marked `disableListColumn`.
### How?
- Call `filterFields` recursively for `tab.fields` and for any field
exposing a `fields` array.
Fixes#13496
### What?
This PR applies `mergeFieldStyles` to the `BlocksField` component,
ensuring that custom admin styles such as `width` are correctly
respected when Blocks fields are placed inside row layouts.
### Why?
Previously, Blocks fields did not inherit or apply their `admin.width`
(or other merged field styles). For example, when placing two Blocks
fields side by side inside a row with `width: '50%'`, the widths were
ignored, causing layout issues.
### How?
- Imported and used `mergeFieldStyles` within `BlocksField`.
- Applied the merged styles to the root `<div>` via the `style` prop,
consistent with how other field components (like `TextField`) handle
styles.
Fixes#13498
## Description
Fixes "Parse Error: Invalid character in Content-Length" errors that
occur when S3-compatible storage providers (like MinIO) return undefined
or invalid ContentLength values.
## Changes
- Added validation before appending Content-Length header in
`staticHandler.ts`
- Only appends Content-Length when value is present and numeric
- Prevents HTTP specification violations from undefined/invalid values
## Code Changes
```typescript
const contentLength = String(object.ContentLength);
if (contentLength && !isNaN(Number(contentLength))) {
headers.append('Content-Length', contentLength);
}
```
## Issue
- Resolves MinIO compatibility issues where undefined ContentLength
causes client parse errors
- Maintains backward compatibility when ContentLength is valid
## Testing
- [x] Tested with MinIO provider returning undefined ContentLength
- [x] Verified valid Content-Length values are still properly set
- [x] Confirmed no regression in existing S3 functionality
### Type of Change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
### Checklist
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings
- [x] Any dependent changes have been merged and published
```
### What?
When exporting, if no `sort` parameter is set but a `groupBy` parameter
is present in the list-view query, the export will treat `groupBy` as
the SortBy field and default to ascending order.
Additionally, the SortOrder field in the export UI is now hidden when no
sort is present, reducing visual noise and preventing irrelevant order
selection.
### Why?
Previously, exports ignored `groupBy` entirely when no sort was set,
leading to unsorted output even if the list view was grouped. Also,
SortOrder was always shown, even when no sort field was selected, which
could be confusing. These changes ensure exports reflect the list view’s
grouping and keep the UI focused.
### How?
- Check for `groupBy` in the query only when `sort` is unset.
- If found, set SortBy to `groupBy` and SortOrder to ascending.
- Hide the SortOrder field when `sort` is not set.
- Leave sorting unset if neither `sort` nor `groupBy` are present.
Currently, if you don't have delete access to the document, the UI
doesn't allow you to replace the file, which isn't expected. This is
also a UI only restriction, and the API allows you do this fine.
This PR makes so the "remove file" button renders even if you don't have
delete access, while still ensures you have update access.
---------
Co-authored-by: Paul Popus <paul@payloadcms.com>
### Issue
The folders join field query was returning trashed documents.
### Fix
Adds a constraint to trash enabled collections, which prevents trashed
documents from being surfaced in folder views.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
Went ahead and bumped the base from `node:22.12.0-alpine` to
`node:22.17.0-alpine` across all templates to fix#13019.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
This PR translates the block and item label in the version view, as well
as the version label in the step nav.
### Why?
To appropriately translate labels in the version views.
### How?
By using the TFunction from useTranslation as well as existing
translation keys.
Fixes#13193Fixes#13194
### What?
Added `white-space: nowrap` to the `.bulk-upload--actions-bar__buttons`
class to ensure button labels remain on a single line.
### Why?
In the bulk upload action bar, buttons containing multi-word labels were
wrapping to two lines, causing them to expand vertically and misalign
with other controls.
### How?
Applied `white-space: nowrap` to `.bulk-upload--actions-bar__buttons` so
all button labels stay on one line, maintaining consistent height and
alignment.
#### Before
<img width="1469" height="525" alt="Screenshot 2025-08-15 at 9 20 07 AM"
src="https://github.com/user-attachments/assets/aecc65ae-7b2f-43ba-96c8-1143fcee7f88"
/>
#### After
<img width="1474" height="513" alt="Screenshot 2025-08-15 at 9 19 55 AM"
src="https://github.com/user-attachments/assets/438c6ee1-b966-4966-8686-37ba4619a25c"
/>
🤖 Automated bump of templates for v3.52.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
### What?
This PR adds a dedicated `sortOrder` select field (Ascending /
Descending) to the import-export plugin, alongside updates to the
existing `SortBy` component. The new field and component logic keep the
sort field (`sort`) in sync with the selected sort direction.
### Why?
Previously, descending sorting did not work. While the `SortBy` field
could read the list view’s `query.sort` param, if the value contained a
leading dash (e.g. `-title`), it would not be handled correctly. Only
ascending sorts such as `sort=title` worked, and the preview table would
not reflect a descending order.
### How?
- Added a new `sortOrder` select field to the export options schema.
- Implemented a `SortOrder` custom component using ReactSelect:
- On new exports, reads `query.sort` from the list view and sets both
`sortOrder` and `sort` (combined value with or without a leading dash).
- Handles external changes to `sort` that include a leading dash.
- Updated `SortBy`:
- No longer writes to `sort` during initial hydration—`SortOrder` owns
initial value setting.
- On user field changes, writes the combined value using the current
`sortOrder`.
### What:
This PR adds `limit` and `page` fields to the export options, allowing
users to control the number of documents exported and the page from
which to start the export. It also enforces that limit must be a
positive multiple of 100.
### Why:
This feature is needed to provide pagination support for large exports,
enabling users to export manageable chunks of data rather than the
entire dataset at once. Enforcing multiples-of-100 for `limit` ensures
consistent chunking behavior and prevents unexpected export issues.
### How:
- The `limit` field determines the maximum number of documents to export
and **must be a positive multiple of 100**.
- The `page` field defines the starting page of the export and is
displayed only when a `limit` is specified.
- If `limit` is cleared, the `page` resets to 1 to maintain consistency.
- Export logic was adjusted to respect the `limit` and `page` values
when fetching documents.
---------
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
### What?
Fixes an issue where CSV exports and the preview table displayed all
fields of documents in hasMany monomorphic relationships instead of only
their IDs.
### Why?
This caused cluttered output and inconsistent CSV formats, since only
IDs should be exported for hasMany monomorphic relationships.
### How?
Added explicit `toCSV` handling for all relationship types in
`getCustomFieldFunctions`, updated `flattenObject` to delegate to these
handlers, and adjusted `getFlattenedFieldKeys` to generate the correct
headers.
🤖 Automated bump of templates for v3.51.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
### What?
Adds validation to the file upload field to ensure a filename is
provided. If the filename is missing, a clear error message is shown to
the user instead of a general error.
### Why?
Currently, attempting to upload a file without a filename results in a
generic error message: `Something went wrong.` This makes it unclear for
users to understand what the issue is.
### How?
The upload field validation has been updated to explicitly check for a
missing filename. If the filename is undefined or null, the error
message `A filename is required` is now shown.
Fixes#13410
### What?
Fixes an issue where using a function as the `label` for a `tabs` field
causes the versions UI to break.
### Why?
The versions UI was not properly resolving function labels on `tab`
fields, leading to a crash when trying to render them.
### How?
Tweaked the logic so that if the label is a function, it gets called
before rendering.
Fixes#13375
Follow-up to #13416. Supersedes #13434.
When autosave is triggered and the user continues to modify fields,
their changes are overridden by the server's value, i.e. the value at
the time the form state request was made. This makes it almost
impossible to edit fields when using a small autosave interval and/or a
slow network.
This is because autosave is now merged into form state, which by default
uses `acceptValues: true`. This does exactly what it sounds like,
accepts all the values from the server—which may be stale if underlying
changes have been made. We ignore these values for onChange events,
because the user is actively making changes. But during form
submissions, we can accept them because the form is disabled while
processing anyway.
This pattern allows us to render "computed values" from the server, i.e.
a field with an `beforeChange` hook that modifies its value.
Autosave, on the other hand, happens in the background _while the form
is still active_. This means changes may have been made since sending
the request. We still need to accept computed values from the server,
but we need to avoid doing this if the user has active changes since the
time of the request.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211027929253429
### What?
Adds e2e tests that verify group-by functionality within the trash view
of a collection.
### Why?
To ensure group-by behaves correctly when viewing soft-deleted
documents, including:
- Clearing the group-by selection via the reset button.
- Navigating from grouped rows to the trashed document's edit view.
### How?
- Added `should properly clear group-by in trash view` to test the reset
button behavior.
- Added `should properly navigate to trashed doc edit view from group-by
in trash view` to confirm correct linking and routing.
By default, `payload.jobs.run` only runs jobs from the `default` queue
(since https://github.com/payloadcms/payload/pull/12799). It exposes an
`allQueues` property to run jobs from all queues.
For handling schedules (`payload.jobs.handleSchedules` and
`config.jobs.autoRun`), this behaves differently - jobs are run from all
queues by default, and no `allQueues` property exists.
This PR adds an `allQueues` property to scheduling, as well as changes
the default behavior to only handle schedules for the `default` queue.
That way, the behavior of running and scheduling jobs matches.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210982048221260
Allows user to override more of the tenant field config. Now you can
override most of the field config with:
### At the root level
```ts
/**
* Field configuration for the field added to all tenant enabled collections
*/
tenantField?: RootTenantFieldConfigOverrides
```
### At the collection level
Setting collection level overrides will replace the root level overrides
shown above.
```ts
collections: {
[key in CollectionSlug]?: {
// ... rest of the types
/**
* Overrides for the tenant field, will override the entire tenantField configuration
*/
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
}
}
```
### What?
This PR contains a couple of fixes to the bulk upload process:
- Credentials not being passed when fetching, leading to auth issues
- Provide a fallback to crypto.randomUUID which is only available when
using HTTPS or localhost
### Why?
I use [separate admin and API URLs](#12682) and work off a remote dev
server using custom hostnames. These issues may not impact the happy
path of using localhost, but are dealbreakers in this environment.
### Fixes #
_These are issues I found myself, I fixed them rather than raising
issues for somebody else to pick up, but I can create issues to link to
if required._
Currently, the slug field is generated from the title field
indefinitely, even after the document has been created and the initial
slug has been assigned. This should only occur on create, however, as it
currently does, or when the user explicitly requests it.
Given that slugs often determine the URL structure of the webpage that
the document corresponds to, they should rarely change after being
published, and when they do, would require HTTP redirects, etc. to do
right in a production environment.
But this is also a problem with Live Preview which relies on a constant
iframe src. If your Live Preview URL includes the slug as a route param,
which is often the case, then changing the slug will result in a broken
connection as the queried document can no longer be found. The current
workaround is to save the document and refresh the page.
Now, the slug is only generated on initial create, or when the user
explicitly clicks the new "generate" button above the slug field. In the
future we can evaluate supporting dynamic Live Preview URLs.
Regenerating this URL on every change would put additional load on the
client as it would have to reestablish connection every time it changes,
but it should be supported still. See #13055.
Discord discussion here:
https://discord.com/channels/967097582721572934/1102950643259424828/1387737976892686346
Related: #10536
Fixes#10515. Needed for #12956.
Hooks run within autosave are not reflected in form state.
Similar to #10268, but for autosave events.
For example, if you are using a computed value, like this:
```ts
[
// ...
{
name: 'title',
type: 'text',
},
{
name: 'computedTitle',
type: 'text',
hooks: {
beforeChange: [({ data }) => data?.title],
},
},
]
```
In the example above, when an autosave event is triggered after changing
the `title` field, we expect the `computedTitle` field to match. But
although this takes place on the database level, the UI does not reflect
this change unless you refresh the page or navigate back and forth.
Here's an example:
Before:
https://github.com/user-attachments/assets/c8c68a78-9957-45a8-a710-84d954d15bcc
After:
https://github.com/user-attachments/assets/16cb87a5-83ca-4891-b01f-f5c4b0a34362
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210561273449855
## What
Before this PR, an internal link in the Lexical editor could reference a
document from a different tenant than the active one.
Reproduction:
1. `pnpm dev plugin-multi-tenant`
2. Log in with `dev@payloadcms.com` and password `test`
3. Go to `http://localhost:3000/admin/collections/food-items` and switch
between the `Blue Dog` and `Steel Cat` tenants to see which food items
each tenant has.
4. Go to http://localhost:3000/admin/collections/food-items/create, and
in the new richtext field enter an internal link
5. In the relationship select menu, you will see the 6 food items at
once (3 of each of those tenants). In the relationship select menu, you
would previously see all 6 food items at once (3 from each of those
tenants). Now, you'll only see the 3 from the active tenant.
The new test verifies that this is fixed.
## How
`baseListFilter` is used, but now it's called `baseFilter` for obvious
reasons: it doesn't just filter the List View. Having two different
properties where the same function was supposed to be placed wasn't
feasible. `baseListFilter` is still supported for backwards
compatibility. It's used as a fallback if `baseFilter` isn't defined,
and it's documented as deprecated.
`baseFilter` is injected into `filterOptions` of the internal link field
in the Lexical Editor.
When grouping by a checkbox field, boolean values are not translated,
causing labels to render incorrectly, and falsey values to render
without a heading.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210979856538211
A comprehensive revision has been made to correct the majority of
localization translation errors. Previous versions were often
grammatically incorrect and awkward. This update includes a one-time
overhaul to improve grammar, vocabulary, and fix a few terms that were
written in Simplified Chinese.
Since a large number of translated terms have been corrected, it is
recommended to use GitHub's Files changed feature to review the diffs
directly.
This Pull Request only modifies translation content; no other code
changes have been made.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
When grouping by a date field and its value is null, the list view
crashes.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210979856538208
### What?
Fix the folder view for upload documents only using
`formatFolderOrDocumentItem()` function and only if the upload is an
image, even when there's a `thumbnailURL` available.
### Why?
Folder view for upload collections (especially those with sharp resizing
disabled) renders different thumbnails between the folder view and list
view. With sharp resizing disabled and an `adminThumbnail` fn provided,
the list view will correctly render optimised images, while the folder
view renders full source images - resulting in a huge discrepancy in
loaded image sizes.
### How?
We're passing the `value.thumbnailURL` **before** the
`formatFolderOrDocumentItem()` call rather than passing it directly as a
function parameter to cover cases where non-image uploads have a
`thumbnailURL` defined.
Fixes#13246
### What?
Allows document to successfully be saved when `fallback to default
locale` checked without throwing an error.
### Why?
The `fallback to default locale` checkbox allows users to successfully
save a document in the admin panel while using fallback data for
required fields, this has been broken since the release of `v3`.
Without the checkbox override, the user would be prevented from saving
the document in the UI because the field is required and will throw an
error.
The logic of using fallback data is not affected by this checkbox - it
is purely to allow saving the document in the UI.
### How?
The `fallback` checkbox used to have an `onChange` function that
replaces the field value with null, allowing it to get processed through
the standard localization logic and get replaced by fallback data.
However, this `onChange` was removed at some point and the field was
passing the actual checkbox value `true`/`false` which then breaks the
form and prevent it from saving.
This fallback checkbox is only displayed when `fallback: true` is set in
the localization config.
This PR also updated the checkbox to only be displayed when `required:
true` - when it's the field is not `required` this checkbox serves no
purpose.
Also adds tests to `localization/e2e`.
Fixes#11245
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
### What?
Updated `TypeWithID` so `deletedAt` can accept `null`.
### Why?
Generated collection types for trash use:
```
deletedAt?: string | null
```
`TypeWithID` previously only allowed `string | undefined`, so assigning
documents with `deletedAt: null` caused TypeScript errors.
Aligning the types fixes this mismatch and ensures `TypeWithID` is
compatible with the generated types.
### How?
Modified the `TypeWithID` definition to:
```
export type TypeWithID = {
deletedAt?: string | null
docId?: any
id: number | string
}
```
This makes `deletedAt` effectively `string | null | undefined`, matching
generated types and eliminating type errors.
Fixes#13341
Extension of https://github.com/payloadcms/payload/pull/13213
This PR correctly filters tenants, users and documents based on the
users assigned tenants if any are set. If a user is assigned tenants
then list results should only show documents with those tenants (when
selector is not set). Previously you could construct access results that
allows them to see them, but in the confines of the admin panel they
should not see them. If you wanted a user to be able to see a "public"
tenant while inside the admin panel they either need to be added to the
tenant or have no tenants at all.
Note that this is for filtering only, access control still controls what
documents a user has _access_ to a document. The filters are and always
have been a way to filter out results in the list view.
Field-level permissions were not handled correctly at all. If you had a
field set with access control, this would mean that nested fields would
incorrectly be omitted from the version view.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210932060696919
### What?
Optimize the relationship value loading by selecting only the
`useAsTitle` field when fetching document data via the REST API.
### Why?
Previously, all fields were fetched via a POST request when loading the
document data of a relationship value, causing unnecessary data transfer
and slower performance. Only the `useAsTitle` field is needed to display
the related document’s title in the relationship UI field.
### How?
Applied a select to the REST API POST request, similar to how the
options list is loaded, limiting the response to the `useAsTitle` field
only.
Fields such as groups and arrays would not always reset errorPaths when
there were no more errors. The server and client state was not being
merged safely and the client state was always persisting when the server
sent back no errorPaths, i.e. itterable fields with fully valid
children. This change ensures errorPaths is defaulted to an empty array
if it is not present on the incoming field.
Likely a regression from
https://github.com/payloadcms/payload/pull/9388.
Adds e2e test.
### What?
- Updated the `countOperation` to respect the `trash` argument.
### Why?
- Previously, `count` would incorrectly include trashed documents even
when `trash` was not specified.
- This change aligns `count` behavior with `find` and other operations,
providing accurate counts for normal and trashed documents.
### How?
- Applied `appendNonTrashedFilter` in `countOperation` to automatically
exclude soft-deleted docs when `trash: false` (default).
- Added `trash` argument support in Local API, REST API (`/count`
endpoints), and GraphQL (`count<Collection>` queries).
### What?
- Added new end-to-end tests covering trash functionality for
auth-enabled collections (e.g., `users`).
- Implemented test cases for:
- Display of the trash tab in the list view.
- Trashing a user and verifying its appearance in the trash view.
- Accessing the trashed user edit view.
- Ensuring all auth fields are properly disabled in trashed state.
- Restoring a trashed user and verifying its status.
### Why?
- To ensure that the trash (soft-delete) feature works consistently for
collections with `auth: true`.
- To prevent regressions in user management flows, especially around
disabling and restoring trashed users.
### How?
- Added a new `Auth enabled collection` test suite in the E2E `Trash`
tests.
### What?
Fixes an issue where document links in the trash view were incorrectly
generated when group-by was enabled for a collection. Previously, links
in grouped tables would omit the `/trash` segment, causing navigation to
crash while trying to route to the default edit view instead of the
trashed document view.
### Why?
When viewing a collection in group-by mode, document rows are rendered
in grouped tables via the `handleGroupBy` logic. However, these tables
were unaware of whether the view was operating in trash mode, so the
generated row links did not include the necessary `/trash` segment. This
broke navigation when trying to view or edit trashed documents.
### How?
- Threaded the `viewType` prop through `renderListView` into the
`handleGroupBy` utility.
- Passed `viewType` into each `renderTable` call within `handleGroupBy`,
ensuring proper link generation.
- `renderTable` already supports `viewType` and appends `/trash` to edit
links when it's set to 'trash'.
Previously, a single run of the simplest job queue workflow (1 single
task, no db calls by user code in the task - we're just testing db
system overhead) would result in **22 db roundtrips** on drizzle. This
PR reduces it to **17 db roundtrips** by doing the following:
- Modifies db.updateJobs to use the new optimized upsertRow function if
the update is simple
- Do not unnecessarily pass the job log to the final job update when the
workflow completes => allows using the optimized upsertRow function, as
only the main table is involved
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210888186878606
## What?
The Slate to Lexical migration script assumes that the depth of Slate
nodes matches the depth of the Lexical schema, which isn't necessarily
true. This pull request fixes this assumption by first checking for
children and unwrapping the text nodes.
## Why?
During my migration, I ran into a lot of copy + pasted rich text with
list items with untyped nodes with `children`. The existing migration
script assumed that since list items can't have paragraphs, all untyped
nodes inside must be text nodes.
The result of the migration script was a lot of invalid text nodes with
`text: undefined` and all of the content in the `children` being
silently lost. Beyond the silent loss, the invalid text nodes caused the
Lexical editor to unmount with an error about accessing `0 of
undefined`, so those documents couldn't be edited.
This additionally makes the migration script more closely align with the
[recursive serialization logic recommendation from the Payload Slate
Rich Text
documentation](https://payloadcms.com/docs/rich-text/slate#generating-html).
## Visualization
### Slate
```txt
Slate rich text content
┣━┳━ Unordered list
┋ ┣━┳━ List item
┋ ┋ ┗━┳━ Generic (paragraph-like, untyped with children)
┋ ┋ ┣━━━ Text (untyped) `Hello `
┋ ┋ ┗━━━ Text (untyped) `World!
[...]
```
### Lexical Before PR
```txt
Lexical rich text content (invalid)
┣━┳━ Unordered list
┋ ┣━┳━ List item
┋ ┋ ┗━━━ Invalid text (assumed the generic node was text, stopped processing children, cannot restore lost text without a restoring backup with Slate and rerunning the script after this MR)
[...]
```
### Lexical After PR
```txt
Lexical rich text content
┣━┳━ Unordered list
┋ ┣━┳━ List item
┋ ┋ ┣━━━ Text `Hello `
┋ ┋ ┗━━━ Text `World!
[...]
```
---------
Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
### What?
- Updated `TrashView` to pass `trash: true` as a dedicated prop instead
of embedding it in the `query` object.
- Modified `renderListView` to correctly merge `trash` and `where`
queries by using both `queryFromArgs` and `queryFromReq`.
- Ensured filtering (via `where`) works correctly in the trash view.
### Why?
Previously, the `trash: true` flag was injected into the `query` object,
and `renderListView` only used `queryFromArgs`.
This caused the `where` clause from filters (added by the
`WhereBuilder`) to be overridden, breaking filtering in the trash view.
### How?
- Introduced an explicit `trash` prop in `renderListView` arguments.
- Updated `TrashView` to pass `trash: true` separately.
- Updated `renderListView` to apply the `trash` filter in addition to
any `where` conditions.
### What?
Prevents decrypted apiKey from being saved back to database on the auth
refresh operation.
### Why?
References issue #13063: refreshing a token for a logged-in user
decrypted `apiKey` and wrote it back in plaintext, corrupting the user
record.
### How?
The user is now fetched with `db.findOne` instead of `findByID`,
preserving the encryption of the key when saved back to the database
using `db.updateOne`. The user record is then re-fetched using
`findByID`, allowing for the decrypted key to be provided in the
response.
### Tests
* ✅ keeps apiKey encrypted in DB after refresh
* ✅ returns user with decrypted apiKey after refresh
Fixes#13063
### What
- filters cookies with the `payload-` prefix in `getExternalFile` by
default (if `externalFileHeaderFilter` is not used).
- Document in `externalFileHeaderFilter`, that the user should handle
the removing of the payload cookie.
### Why
In the Payload application, the `getExternalFile` function sends the
user's cookies to an external server when fetching media, inadvertently
exposing the user's session to that third-party service.
```ts
const headers = uploadConfig.externalFileHeaderFilter
? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers)))
: { cookie: req.headers?.get('cookie') };
const res = await fetch(fileURL, {
credentials: 'include',
headers,
method: 'GET',
});
```
Although the
[externalFileHeaderFilter](https://payloadcms.com/docs/upload/overview#collection-upload-options)
function can strip sensitive cookies from the request, the default
config includes the session cookie, violating the secure-by-default
principle.
### How
- If `externalFileHeaderFilter` is not defined, any cookie beginning
with `payload-` is filtered.
- Added 2 tests: both for the case where `externalFileHeaderFilter` is
defined and for the case where it is not.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210561338171125
### What?
Update Swedish translation, removing minor inconsistencies and opting
for more natural sounding translations
### Why?
The current Swedish translation contained some minor grammatical issues
and inconsistencies that make the UI feel less natural to Swedish users.
### How?
- Fixed "e-post" hyphenation consistency
- Changed "Alla platser" → "Alla språk" (locales should be "languages")
- Improved action verbs: "Tydlig" → "Rensa", "Stänga" → "Stäng"
- Made "Kollapsa" → "Fäll ihop" more natural
- Standardized preview terminology: "Live förhandsgranskning" →
"förhandsgranskning"
- Fixed terminology: "fältdatabas" → "fältdata" (fältdatabas mean field
database while fältdata means field data)
- Changed "Programinställningar" → "Systeminställningar" (more
appropriate for software)
- Fixed punctuation: em dash → comma in "sorryNotFound"
- Improved "Visa endast läsning" → "Visa som skrivskyddad"
(grammatically correct)
Fixes #
When grouping by a relationship field and it's value is `null`, the list
view crashes.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210916642997992
Fixes#12975.
When editing autosave-enabled documents through the join field, the
document drawer closes unexpectedly on every autosave interval, making
it nearly impossible to use.
This is because as of #12842, the underlying relationship table
re-renders on every autosave event, remounting the drawer each time. The
fix is to lift the drawer out of table's rendering tree and into the
join field itself. This way all rows share the same drawer, whose
rendering lifecycle has been completely decoupled from the table's
state.
Note: this is very similar to how relationship fields achieve similar
functionality.
This PR also adds jsdocs to the `useDocumentDrawer` hook and strengthens
its types.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210906078627353
Catches list filter errors and prevents the list view from crashing when
attempting to search on fields the user does not have access to. Instead
just shows the default "no results found" message.
Custom document tab components (server components) do not receive the
`user` prop, as the types suggest. This makes it difficult to wire up
conditional rendering based on the user. This is because tab conditions
don't receive a user argument either, forcing you to render the default
tab component yourself—but a custom component should not be needed for
this in the first place.
Now they both receive `req` alongside `user`, which is more closely
aligned with custom field components.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210906078627357
### What?
- Fixed an issue where group-by enabled collections with `trash: true`
were not showing trashed documents in the collection’s trash view.
- Ensured that the `trash` query argument is properly passed to the
`findDistinct` call within `handleGroupBy`, allowing trashed documents
to be included in grouped list views.
### Why?
Previously, when viewing the trash view of a collection with both
**group-by** and **trash** enabled, trashed documents would not appear.
This was caused by the `trash` argument not being forwarded to
`findDistinct` in `handleGroupBy`, which resulted in empty or incorrect
group-by results.
### How?
- Passed the `trash` flag through all relevant `findDistinct` and `find`
calls in `handleGroupBy`.
- bumps next.js from 15.3.2 to 15.4.4 in monorepo and templates. It's
important to run our tests against the latest Next.js version to
guarantee full compatibility.
- bumps playwright because of peer dependency conflict with next 15.4.4
- bumps react types because why not
https://nextjs.org/blog/next-15-4
As part of this upgrade, the functionality added by
https://github.com/payloadcms/payload/pull/11658 broke. This PR fixes it
by creating a wrapper around `React.isValidElemen`t that works for
Next.js 15.4.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210803039809808
Fixes#13191
- Render a single html element for single error messages
- Preserve ul structure for multiple errors
- Updates tests to check for both cases
Previously, `db.deleteMany` on postgres resulted in 2 roundtrips to the
database (find + delete with ids). This PR passes the where query
directly to the `deleteWhere` function, resulting in only one roundtrip
to the database (delete with where).
If the where query queries other tables (=> joins required), this falls
back to find + delete with ids. However, this is also more optimized
than before, as we now pass `select: { id: true }` to the findMany
query.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210871676349299
Previously, the Lexical editor was using px, and the JSX converter was
using rem. #12848 fixed the inconsistency by changing the editor to rem,
but it should have been the other way around, changing the JSX converter
to px.
You can see the latest explanation about why it should be 40px
[here](https://github.com/payloadcms/payload/issues/13130#issuecomment-3058348085).
In short, that's the default indentation all browsers use for lists.
This time I'm making sure to leave clear comments everywhere and a test
to avoid another regression.
Here is an image of what the e2e test looks like:
<img width="321" height="678" alt="image"
src="https://github.com/user-attachments/assets/8880c7cb-a954-4487-8377-aee17c06754c"
/>
The first part is the Lexical editor, the second is the JSX converter.
As you can see, the checkbox in JSX looks a little odd because it uses
an input checkbox (as opposed to a pseudo-element in the Lexical
editor). I thought about adding an inline style to move it slightly to
the left, but I found that browsers don't have a standard size for the
checkbox; it varies by browser and device.
That requires a little more thought; I'll address that in a future PR.
Fixes#13130
🤖 Automated bump of templates for v3.49.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
When populating the selector it should populate it with assigned tenants
before fetching all tenants that a user has access to.
You may have "public" tenants and while a user may have _access_ to the
tenant, the selector should show the ones they are assigned to. Users
with full access are the ones that should be able to see the public ones
for editing.
Previously, filtering by a polymorphic relationship inside an array /
group (unless the `name` is `version`) / tab caused `QueryError: The
following path cannot be queried:`.
### What?
This PR introduces complete trash (soft-delete) support. When a
collection is configured with `trash: true`, documents can now be
soft-deleted and restored via both the API and the admin panel.
```
import type { CollectionConfig } from 'payload'
const Posts: CollectionConfig = {
slug: 'posts',
trash: true, // <-- New collection config prop @default false
fields: [
{
name: 'title',
type: 'text',
},
// other fields...
],
}
```
### Why
Soft deletes allow developers and admins to safely remove documents
without losing data immediately. This enables workflows like reversible
deletions, trash views, and auditing—while preserving compatibility with
drafts, autosave, and version history.
### How?
#### Backend
- Adds new `trash: true` config option to collections.
- When enabled:
- A `deletedAt` timestamp is conditionally injected into the schema.
- Soft deletion is performed by setting `deletedAt` instead of removing
the document from the database.
- Extends all relevant API operations (`find`, `findByID`, `update`,
`delete`, `versions`, etc.) to support a new `trash` param:
- `trash: false` → excludes trashed documents (default)
- `trash: true` → includes both trashed and non-trashed documents
- To query **only trashed** documents: use `trash: true` with a `where`
clause like `{ deletedAt: { exists: true } }`
- Enforces delete access control before allowing a soft delete via
update or updateByID.
- Disables version restoring on trashed documents (must be restored
first).
#### Admin Panel
- Adds a dedicated **Trash view**: `/collections/:collectionSlug/trash`
- Default delete action now soft-deletes documents when `trash: true` is
set.
- **Delete confirmation modal** includes a checkbox to permanently
delete instead.
- Trashed documents:
- Displays UI banner for better clarity of trashed document edit view vs
non-trashed document edit view
- Render in a read-only edit view
- Still allow access to **Preview**, **API**, and **Versions** tabs
- Updated Status component:
- Displays “Previously published” or “Previously a draft” for trashed
documents.
- Disables status-changing actions when documents are in trash.
- Adds new **Restore** bulk action to clear the `deletedAt` timestamp.
- New `Restore` and `Permanently Delete` buttons for
single-trashed-document restore and permanent deletion.
- **Restore confirmation modal** includes a checkbox to restore as
`published`, defaults to `draft`.
- Adds **Empty Trash** and **Delete permanently** bulk actions.
#### Notes
- This feature is completely opt-in. Collections without trash: true
behave exactly as before.
https://github.com/user-attachments/assets/00b83f8a-0442-441e-a89e-d5dc1f49dd37
## Problem:
In PR #11887, a bug fix for `copyToLocale` was introduced to address
issues with copying content between locales in Postgres. However, an
incorrect algorithm was used, which removed all "id" properties from
documents being copied. This led to bug #12536, where `copyToLocale`
would mistakenly delete the document in the source language, affecting
not only Postgres but any database.
## Cause and Solution:
When copying documents with localized arrays or blocks, Postgres throws
errors if there are two blocks with the same ID. This is why PR #11887
removed all IDs from the document to avoid conflicts. However, this
removal was too broad and caused issues in cases where it was
unnecessary.
The correct solution should remove the IDs only in nested fields whose
ancestors are localized. The reasoning is as follows:
- When an array/block is **not localized** (`localized: false`), if it
contains localized fields, these fields share the same ID across
different locales.
- When an array/block **is localized** (`localized: true`), its
descendant fields cannot share the same ID across different locales if
Postgres is being used. This wouldn't be an issue if the table
containing localized blocks had a composite primary key of `locale +
id`. However, since the primary key is just `id`, we need to assign a
new ID for these fields.
This PR properly removes IDs **only for nested fields** whose ancestors
are localized.
Fixes#12536
## Example:
### Before Fix:
```js
// Original document (en)
array: [{
id: "123",
text: { en: "English text" }
}]
// After copying to 'es' locale, a new ID was created instead of updating the existing item
array: [{
id: "456", // 🐛 New ID created!
text: { es: "Spanish text" } // 🐛 'en' locale is missing
}]
```
### After fix:
```js
// After fix
array: [{
id: "123", // ✅ Same ID maintained
text: {
en: "English text",
es: "Spanish text" // ✅ Properly merged with existing item
}
}]
```
## Additional fixes:
### TraverseFields
In the process of designing an appropriate solution, I detected a couple
of bugs in traverseFields that are also addressed in this PR.
### Fixed MongoDB Empty Array Handling
During testing, I discovered that MongoDB and PostgreSQL behave
differently when querying documents that don't exist in a specific
locale:
- PostgreSQL: Returns the document with data from the fallback locale
- MongoDB: Returns the document with empty arrays for localized fields
This difference caused `copyToLocale` to fail in MongoDB because the
merge algorithm only checked for `null` or `undefined` values, but not
empty arrays. When MongoDB returned `content: []` for a non-existent
locale, the algorithm would attempt to iterate over the empty array
instead of using the source locale's data.
### Move test e2e to int
The test introduced in #11887 didn't catch the bug because our e2e suite
doesn't run on Postgres. I migrated the test to an integration test that
does run on Postgres and MongoDB.
Supports grouping documents by specific fields within the list view.
For example, imagine having a "posts" collection with a "categories"
field. To report on each specific category, you'd traditionally filter
for each category, one at a time. This can be quite inefficient,
especially with large datasets.
Now, you can interact with all categories simultaneously, grouped by
distinct values.
Here is a simple demonstration:
https://github.com/user-attachments/assets/0dcd19d2-e983-47e6-9ea2-cfdd2424d8b5
Enable on any collection by setting the `admin.groupBy` property:
```ts
import type { CollectionConfig } from 'payload'
const MyCollection: CollectionConfig = {
// ...
admin: {
groupBy: true
}
}
```
This is currently marked as beta to gather feedback while we reach full
stability, and to leave room for API changes and other modifications.
Use at your own risk.
Note: when using `groupBy`, bulk editing is done group-by-group. In the
future we may support cross-group bulk editing.
Dependent on #13102 (merged).
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210774523852467
---------
Co-authored-by: Paul Popus <paul@payloadcms.com>
### What?
Refactors the `LeaveWithoutSaving` modal to be generic and delegates
document unlock logic back to the `DefaultEditView` component via a
callback.
### Why?
Previously, `unlockDocument` was triggered in a cleanup `useEffect` in
the edit view. When logging out from the edit view, the unlock request
would often fail due to the session ending — leaving the document in a
locked state.
### How?
- Introduced `onConfirm` and `onPrevent` props for `LeaveWithoutSaving`.
- Moved all document lock/unlock logic into `DefaultEditView`’s
`handleLeaveConfirm`.
- Captures the next navigation target via `onPrevent` and evaluates
whether to unlock based on:
- Locking being enabled.
- Current user owning the lock.
- Navigation not targeting internal admin views (`/preview`, `/api`,
`/versions`).
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
### What?
Improves both the JSON preview and export functionality in the
import-export plugin:
- Preserves proper nesting of object and array fields (e.g., groups,
tabs, arrays)
- Excludes any fields explicitly marked as `disabled` via
`custom.plugin-import-export`
- Ensures downloaded files use proper JSON formatting when `format` is
`json` (no CSV-style flattening)
### Why?
Previously:
- The JSON preview flattened all fields to a single level and included
disabled fields.
- Exported files with `format: json` were still CSV-style data encoded
as `.json`, rather than real JSON.
### How?
- Refactored `/preview-data` JSON handling to preserve original document
shape.
- Applied `removeDisabledFields` to clean nested fields using
dot-notation paths.
- Updated `createExport` to skip `flattenObject` for JSON formats, using
a nested JSON filter instead.
- Fixed streaming and buffered export paths to output valid JSON arrays
when `format` is `json`.
~~Sometimes, drizzle is adding the same join to the joins array twice
(`addJoinTable`), despite the table being the same. This is due to a bug
in `getNameFromDrizzleTable` where it would sometimes return a UUID
instead of the table name.~~
~~This PR changes it to read from the drizzle:BaseName symbol instead,
which is correctly returning the table name in my testing. It falls back
to `getTableName`, which uses drizzle:Name.~~
This for some reason fails the tests. Instead, this PR just uses the
getTableName utility now instead of searching for the symbol manually.
https://github.com/payloadcms/payload/pull/13186 actually made the
select API _more powerful_, as it can reduce the amount of db calls even
for complex collections with blocks down to 1.
This PR adds a test that verifies this.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210871676349303
Prevents base list filters from being injected into the URL.
This is a problem with the multi-tenant plugin, for example, where
changing the tenant adds a `baseListFilter` to the query, but should
never be exposed to the end user.
Introduced in #13200.
Currently, an optimized DB update (simple data => no
delete-and-create-row) does the following:
1. sql UPDATE
2. sql SELECT
This PR reduces this further to one single DB call for simple
collections:
1. sql UPDATE with RETURNING()
This only works for simple collections that do not have any fields that
need to be fetched from other tables. If a collection has fields like
relationship or blocks, we'll need that separate SELECT call to join in
the other tables.
In 4.0, we can remove all "complex" fields from the jobs collection and
replace them with a JSON field to make use of this optimization
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210803039809814
### What?
Replaces all `payload.logger.info` calls with `payload.logger.debug` in
the `createExport` function.
### Why?
info logs are too verbose. Using debug ensures detailed logs.
### How?
- Updated all logger calls in `createExport` to use `debug` instead of
`info`.
Currently, with DocumentDB instead of a friendly error like "Value must
be unique" we see a generic "Something went wrong" message.
This PR fixes that by adding a fallback to parse the message instead of
using `error.keyValue` which doesn't exist for responses from
DocumentDB.
### What?
A comma is missing in the example code. This results in not valid JSON.
### Why?
I stumbled upon it, while setting up a Tenant-based Payload for the
first time.
### How?
Adding a comma results in valid JSON.
Fixes #
Added a comma. ;)
Fixes#13113
### How?
Does not rely on JS falseyness, instead explicitly checking for null &
undefined
I'm not actually certain this is the approach we want to take. Some
people might interpret "required" as not null, not-undefined and min
length > 1 in the case of strings. If they do, this change to the
behavior in the not-required case will break their expectations
### What?
This PR ensures that when a document is created using the `Publish in
__` button, it is saved to the correct locale.
### Why?
During document creation, the buttons `Publish` or `Publish in [locale]`
have the same effect. As a result, we overlooked the case where a user
may specifically click `Publish in [locale]` for the first save. In this
scenario, the create operation does not respect the
`publishSpecificLocale` value, so the document was always saved in the
default locale regardless of the intended one.
### How?
Passes the `publishSpecificLocale` value to the create operation,
ensuring the document and version is saved to the correct locale.
**Fixes:** #13117
Relational databases were broken with folders because it was querying
on:
```ts
{
folderType: {
equals: []
}
}
```
Which does not work since the select hasMany stores values in a separate
table.
prettier doesn't seem to cover that, and horizontal scrolling in the
browser is even more annoying than in the IDE.
Regex used in the search engine: `^[ \t]*\* `
Some search params within the list view do not properly sync to user
preferences, and visa versa.
For example, when selecting a query preset, the `?preset=123` param is
injected into the URL and saved to preferences, but when reloading the
page without the param, that preset is not reactivated as expected.
### Problem
The reason this wasn't working before is that omitting this param would
also reset prefs. It was designed this way in order to support
client-side resets, e.g. clicking the query presets "x" button.
This pattern would never work, however, because this means that every
time the user navigates to the list view directly, their preference is
cleared, as no param would exist in the query.
Note: this is not an issue with _all_ params, as not all are handled in
the same way.
### Solution
The fix is to use empty values instead, e.g. `?preset=`. When the server
receives this, it knows to clear the pref. If it doesn't exist at all,
it knows to load from prefs. And if it has a value, it saves to prefs.
On the client, we sanitize those empty values back out so they don't
appear in the URL in the end.
This PR also refactors much of the list query context and its respective
provider to be significantly more predictable and easier to work with,
namely:
- The `ListQuery` type now fully aligns with what Payload APIs expect,
e.g. `page` is a number, not a string
- The provider now receives a single `query` prop which matches the
underlying context 1:1
- Propagating the query from the server to the URL is significantly more
predictable
- Any new props that may be supported in the future will automatically
work
- No more reconciling `columns` and `listPreferences.columns`, its just
`query.columns`
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210827129744922
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
-->
### What?
Fixes a bug where `afterChange` hooks would attempt to access values for
fields that are `read: false` but `create: true`, resulting in
`undefined` values and unexpected behavior.
### Why?
In scenarios where access control allows field creation (`create: true`)
but disallows reading it (`read: false`), hooks like `afterChange` would
still attempt to operate on `undefined` values from `siblingDoc` or
`previousDoc`, potentially causing errors or skipped logic.
### How?
Adds safe optional chaining and fallback object initialization in
`promise.ts` for:
- `previousDoc[field.name]`
- `siblingDoc[field.name]`
- Group, Array, and Block field traversals
This ensures that these values are treated as empty objects or arrays
where appropriate to prevent runtime errors during traversal or hook
execution.
Fixes https://github.com/payloadcms/payload/issues/12660
---------
Co-authored-by: Niall Bambury <niall.bambury@cuckoo.co>
Adds a new `schedule` property to workflow and task configs that can be
used to have Payload automatically _queue_ jobs following a certain
_schedule_.
Docs:
https://payloadcms.com/docs/dynamic/jobs-queue/schedules?branch=feat/schedule-jobs
## API Example
```ts
export default buildConfig({
// ...
jobs: {
// ...
scheduler: 'manual', // Or `cron` if you're not using serverless. If `manual` is used, then user needs to set up running /api/payload-jobs/handleSchedules or payload.jobs.handleSchedules in regular intervals
tasks: [
{
schedule: [
{
cron: '* * * * * *',
queue: 'autorunSecond',
// Hooks are optional
hooks: {
// Not an array, as providing and calling `defaultBeforeSchedule` would be more error-prone if this was an array
beforeSchedule: async (args) => {
// Handles verifying that there are no jobs already scheduled or processing.
// You can override this behavior by not calling defaultBeforeSchedule, e.g. if you wanted
// to allow a maximum of 3 scheduled jobs in the queue instead of 1, or add any additional conditions
const result = await args.defaultBeforeSchedule(args)
return {
...result,
input: {
message: 'This task runs every second',
},
}
},
afterSchedule: async (args) => {
await args.defaultAfterSchedule(args) // Handles updating the payload-jobs-stats global
args.req.payload.logger.info(
'EverySecond task scheduled: ' +
(args.status === 'success' ? args.job.id : 'skipped or failed to schedule'),
)
},
},
},
],
slug: 'EverySecond',
inputSchema: [
{
name: 'message',
type: 'text',
required: true,
},
],
handler: ({ input, req }) => {
req.payload.logger.info(input.message)
return {
output: {},
}
},
}
]
}
})
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210495300843759
### What?
Exit the process after running jobs.
### Why?
When running the `payload jobs:run` bin script with a postgres database
the process hangs forever.
### How?
Execute `process.exit(0)` after running all jobs.
### What?
Fixes the `custom.plugin-import-export.disabled` flag to correctly
disable fields in all nested structures including:
- Groups
- Arrays
- Tabs
- Blocks
Previously, only top-level fields or direct children were respected.
This update ensures nested paths (e.g. `group.array.field1`,
`blocks.hero.title`, etc.) are matched and filtered from exports.
### Why?
- Updated regex logic in both `createExport` and Preview components to
recursively support:
- Indexed array fields (e.g. `array_0_field1`)
- Block fields with slugs (e.g. `blocks_0_hero_title`)
- Nested field accessors with correct part-by-part expansion
### How?
To allow users to disable entire field groups or deeply nested fields in
structured layouts.
As discussed in [this
RFC](https://github.com/payloadcms/payload/discussions/12729), this PR
supports collection-scoped folders. You can scope folders to multiple
collection types or just one.
This unlocks the possibility to have folders on a per collection instead
of always being shared on every collection. You can combine this feature
with the `browseByFolder: false` to completely isolate a collection from
other collections.
Things left to do:
- [x] ~~Create a custom react component for the selecting of
collectionSlugs to filter out available options based on the current
folders parameters~~
https://github.com/user-attachments/assets/14cb1f09-8d70-4cb9-b1e2-09da89302995
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210564397815557
Exports `$createLinkNode`, `$isLinkNode` and the equivalent modules for
autolinks.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210710489889573
Adds a new operation findDistinct that can give you distinct values of a
field for a given collection
Example:
Assume you have a collection posts with multiple documents, and some of
them share the same title:
```js
// Example dataset (some titles appear multiple times)
[
{ title: 'title-1' },
{ title: 'title-2' },
{ title: 'title-1' },
{ title: 'title-3' },
{ title: 'title-2' },
{ title: 'title-4' },
{ title: 'title-5' },
{ title: 'title-6' },
{ title: 'title-7' },
{ title: 'title-8' },
{ title: 'title-9' },
]
```
You can now retrieve all unique title values using findDistinct:
```js
const result = await payload.findDistinct({
collection: 'posts',
field: 'title',
})
console.log(result.values)
// Output:
// [
// 'title-1',
// 'title-2',
// 'title-3',
// 'title-4',
// 'title-5',
// 'title-6',
// 'title-7',
// 'title-8',
// 'title-9'
// ]
```
You can also limit the number of distinct results:
```js
const limitedResult = await payload.findDistinct({
collection: 'posts',
field: 'title',
sortOrder: 'desc',
limit: 3,
})
console.log(limitedResult.values)
// Output:
// [
// 'title-1',
// 'title-2',
// 'title-3'
// ]
```
You can also pass a `where` query to filter the documents.
Enhance field-level access controls on Users collection to address
security concerns
- Restricted read/update access on `email` field to admins and the user
themselves
- Locked down `roles` field so only admins can create, read, or update
it
### What?
Adds four more arguments to the `mongooseAdapter`:
```typescript
useJoinAggregations?: boolean /* The big one */
useAlternativeDropDatabase?: boolean
useBigIntForNumberIDs?: boolean
usePipelineInSortLookup?: boolean
```
Also export a new `compatabilityOptions` object from
`@payloadcms/db-mongodb` where each key is a mongo-compatible database
and the value is the recommended `mongooseAdapter` settings for
compatability.
### Why?
When using firestore and visiting
`/admin/collections/media/payload-folders`, we get:
```
MongoServerError: invalid field(s) in lookup: [let, pipeline], only lookup(from, localField, foreignField, as) is supported
```
Firestore doesn't support the full MongoDB aggregation API used by
Payload which gets used when building aggregations for populating join
fields.
There are several other compatability issues with Firestore:
- The invalid `pipeline` property is used in the `$lookup` aggregation
in `buildSortParams`
- Firestore only supports number IDs of type `Long`, but Mongoose
converts custom ID fields of type number to `Double`
- Firestore does not support the `dropDatabase` command
- Firestore does not support the `createIndex` command (not addressed in
this PR)
### How?
```typescript
useJoinAggregations?: boolean /* The big one */
```
When this is `false` we skip the `buildJoinAggregation()` pipeline and resolve the join fields through multiple queries. This can potentially be used with AWS DocumentDB and Azure Cosmos DB to support join fields, but I have not tested with either of these databases.
```typescript
useAlternativeDropDatabase?: boolean
```
When `true`, monkey-patch (replace) the `dropDatabase` function so that
it calls `collection.deleteMany({})` on every collection instead of
sending a single `dropDatabase` command to the database
```typescript
useBigIntForNumberIDs?: boolean
```
When `true`, use `mongoose.Schema.Types.BigInt` for custom ID fields of type `number` which converts to a firestore `Long` behind the scenes
```typescript
usePipelineInSortLookup?: boolean
```
When `false`, modify the sortAggregation pipeline in `buildSortParams()` so that we don't use the `pipeline` property in the `$lookup` aggregation. Results in slightly worse performance when sorting by relationship properties.
### Limitations
This PR does not add support for transactions or creating indexes in firestore.
### Fixes
Fixed a bug (and added a test) where you weren't able to sort by multiple properties on a relationship field.
### Future work
1. Firestore supports simple `$lookup` aggregations but other databases might not. Could add a `useSortAggregations` property which can be used to disable aggregations in sorting.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
This is a follow-up to https://github.com/payloadcms/payload/pull/13060.
There are a bunch of other db adapter methods that use `upsertRow` for
updates: `updateGlobal`, `updateGlobalVersion`, `updateJobs`,
`updateMany`, `updateVersion`.
The previous PR had the logic for using the optimized row updating logic
inside the `updateOne` adapter. This PR moves that logic to the original
`upsertRow` function. Benefits:
- all the other db methods will benefit from this massive optimization
as well. This will be especially relevant for optimizing postgres job
queue initial updates - we should be able to close
https://github.com/payloadcms/payload/pull/11865 after another follow-up
PR
- easier to read db adapter methods due to less code.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210803039809810
Based on https://github.com/payloadcms/payload/pull/13060 which should
be merged first
This PR adds ability to update number fields atomically, which could be
important with parallel writes. For now we support this only via
`payload.db.updateOne`.
For example:
```js
// increment by 10
const res = await payload.db.updateOne({
data: {
number: {
$inc: 10,
},
},
collection: 'posts',
where: { id: { equals: post.id } },
})
// decrement by 3
const res2 = await payload.db.updateOne({
data: {
number: {
$inc: -3,
},
},
collection: 'posts',
where: { id: { equals: post.id } },
})
```
### What?
Fixes the export field selection dropdown to correctly differentiate
between fields in named and unnamed tabs.
### Why?
Previously, when a `tabs` field contained both named and unnamed tabs,
subfields with the same `name` would appear as duplicates in the
dropdown (e.g. `Tab To CSV`, `Tab To CSV`). Additionally, selecting a
field from a named tab would incorrectly map it to the unnamed version
due to shared labels and missing path prefixes.
### How?
- Updated the `reduceFields` utility to manually construct the field
path and label using the tab’s `name` if present.
- Ensured unnamed tabs treat subfields as top-level and skip prefixing
altogether.
- Adjusted label prefix logic to show `Named Tab > Field Name` when
appropriate.
#### Before
<img width="169" height="79" alt="Screenshot 2025-07-15 at 2 55 14 PM"
src="https://github.com/user-attachments/assets/2ab2d19e-41a3-4be2-8496-1da2a79f88e1"
/>
#### After
<img width="211" height="79" alt="Screenshot 2025-07-15 at 2 50 38 PM"
src="https://github.com/user-attachments/assets/0620e96a-71cd-4eb1-9396-30d461ed47a5"
/>
Previously, we were always initializing cronjobs when calling
`getPayload` or `payload.init`.
This is undesired in bin scripts - we don't want cron jobs to start
triggering db calls while we're running an initial migration using
`payload migrate` for example. This has previously led to a race
condition, triggering the following, occasional error, if job autoruns
were enabled:
```ts
DrizzleQueryError: Failed query: select "payload_jobs"."id", "payload_jobs"."input", "payload_jobs"."completed_at", "payload_jobs"."total_tried", "payload_jobs"."has_error", "payload_jobs"."error", "payload_jobs"."workflow_slug", "payload_jobs"."task_slug", "payload_jobs"."queue", "payload_jobs"."wait_until", "payload_jobs"."processing", "payload_jobs"."updated_at", "payload_jobs"."created_at", "payload_jobs_log"."data" as "log" from "payload_jobs" "payload_jobs" left join lateral (select coalesce(json_agg(json_build_array("payload_jobs_log"."_order", "payload_jobs_log"."id", "payload_jobs_log"."executed_at", "payload_jobs_log"."completed_at", "payload_jobs_log"."task_slug", "payload_jobs_log"."task_i_d", "payload_jobs_log"."input", "payload_jobs_log"."output", "payload_jobs_log"."state", "payload_jobs_log"."error") order by "payload_jobs_log"."_order" asc), '[]'::json) as "data" from (select * from "payload_jobs_log" "payload_jobs_log" where "payload_jobs_log"."_parent_id" = "payload_jobs"."id" order by "payload_jobs_log"."_order" asc) "payload_jobs_log") "payload_jobs_log" on true where ("payload_jobs"."completed_at" is null and ("payload_jobs"."has_error" is null or "payload_jobs"."has_error" <> $1) and "payload_jobs"."processing" = $2 and ("payload_jobs"."wait_until" is null or "payload_jobs"."wait_until" < $3) and "payload_jobs"."queue" = $4) order by "payload_jobs"."created_at" asc limit $5
params: true,false,2025-07-10T21:25:03.002Z,autorunSecond,100
at NodePgPreparedQuery.queryWithCache (/Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/pg-core/session.ts:74:11)
at processTicksAndRejections (node:internal/process/task_queues:105:5)
at /Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/node-postgres/session.ts:154:19
... 6 lines matching cause stack trace ...
at N._trigger (/Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/croner@9.0.0/node_modules/croner/dist/croner.cjs:1:16806) {
query: `select "payload_jobs"."id", "payload_jobs"."input", "payload_jobs"."completed_at", "payload_jobs"."total_tried", "payload_jobs"."has_error", "payload_jobs"."error", "payload_jobs"."workflow_slug", "payload_jobs"."task_slug", "payload_jobs"."queue", "payload_jobs"."wait_until", "payload_jobs"."processing", "payload_jobs"."updated_at", "payload_jobs"."created_at", "payload_jobs_log"."data" as "log" from "payload_jobs" "payload_jobs" left join lateral (select coalesce(json_agg(json_build_array("payload_jobs_log"."_order", "payload_jobs_log"."id", "payload_jobs_log"."executed_at", "payload_jobs_log"."completed_at", "payload_jobs_log"."task_slug", "payload_jobs_log"."task_i_d", "payload_jobs_log"."input", "payload_jobs_log"."output", "payload_jobs_log"."state", "payload_jobs_log"."error") order by "payload_jobs_log"."_order" asc), '[]'::json) as "data" from (select * from "payload_jobs_log" "payload_jobs_log" where "payload_jobs_log"."_parent_id" = "payload_jobs"."id" order by "payload_jobs_log"."_order" asc) "payload_jobs_log") "payload_jobs_log" on true where ("payload_jobs"."completed_at" is null and ("payload_jobs"."has_error" is null or "payload_jobs"."has_error" <> $1) and "payload_jobs"."processing" = $2 and ("payload_jobs"."wait_until" is null or "payload_jobs"."wait_until" < $3) and "payload_jobs"."queue" = $4) order by "payload_jobs"."created_at" asc limit $5`,
params: [ true, false, '2025-07-10T21:25:03.002Z', 'autorunSecond', 100 ],
cause: error: relation "payload_jobs" does not exist
at /Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/pg@8.16.3/node_modules/pg/lib/client.js:545:17
at processTicksAndRejections (node:internal/process/task_queues:105:5)
at /Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/node-postgres/session.ts:161:13
at NodePgPreparedQuery.queryWithCache (/Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/pg-core/session.ts:72:12)
at /Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/node-postgres/session.ts:154:19
at find (/Users/alessio/Documents/GitHub/payload2/packages/drizzle/src/find/findMany.ts:162:19)
at Object.updateMany (/Users/alessio/Documents/GitHub/payload2/packages/drizzle/src/updateJobs.ts:26:16)
at updateJobs (/Users/alessio/Documents/GitHub/payload2/packages/payload/src/queues/utilities/updateJob.ts:102:37)
at runJobs (/Users/alessio/Documents/GitHub/payload2/packages/payload/src/queues/operations/runJobs/index.ts:181:25)
at Object.run (/Users/alessio/Documents/GitHub/payload2/packages/payload/src/queues/localAPI.ts:137:12)
at N.fn (/Users/alessio/Documents/GitHub/payload2/packages/payload/src/index.ts:866:13)
at N._trigger (/Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/croner@9.0.0/node_modules/croner/dist/croner.cjs:1:16806) {
length: 112,
severity: 'ERROR',
code: '42P01',
detail: undefined,
hint: undefined,
position: '406',
internalPosition: undefined,
internalQuery: undefined,
where: undefined,
schema: undefined,
table: undefined,
column: undefined,
dataType: undefined,
constraint: undefined,
file: 'parse_relation.c',
line: '1449',
routine: 'parserOpenTable'
}
}
```
This PR makes running crons opt-in using a new `cron` flag. By default,
no cron jobs will be created.
Fixes#7799
Fixes a type issue where all fields in RenderFields['fields'] admin
properties were being marked as required since we were using `Pick`.
Adds a helper type to allow extracting properties with correct
optionality.
Payload is designed with performance in mind, but its customizability
means that there are many ways to configure your app that can impact
performance.
While Payload provides several features and best practices to help you
optimize your app's specific performance needs, these are not currently
well surfaced and can be obscure.
Now:
- A high-level performance doc now exists at `/docs/performance`
- There's a new section on performance within the `/docs/queries` doc
- There's a new section on performance within the `/docs/hooks` doc
- There's a new section on performance within the
`/docs/custom-components` doc
This PR also:
- Restructures and elaborates on the `/docs/queries/pagination` docs
- Adds a new `/docs/database/indexing` doc
- More
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210743577153856
### What?
Adds support for excluding specific fields from the import-export plugin
using a custom field config.
### Why?
Some fields should not be included in exports or previews. This feature
allows users to flag those fields directly in the field config.
### How?
- Introduced a `plugin-import-export.disabled: true` custom field
property.
- Automatically collects and stores disabled field accessors in
`collection.admin.custom['plugin-import-export'].disabledFields`.
- Excludes these fields from the export field selector, preview table,
and final export output (CSV/JSON).
🤖 Automated bump of templates for v3.47.0
Triggered by user: @AlessioGr
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
### What?
Adds a new `format` option to the `plugin-import-export` config that
allows users to force the export format (`csv` or `json`) and hide the
format dropdown from the export UI.
### Why?
In some use cases, allowing the user to select between CSV and JSON is
unnecessary or undesirable. This new option allows plugin consumers to
lock the format and simplify the export interface.
### How?
- Added a `format?: 'csv' | 'json'` field to `ImportExportPluginConfig`.
- When defined, the `format` field in the export UI is:
- Hidden via `admin.condition`
- Pre-filled via `defaultValue`
- Updated `getFields` to accept the plugin config and apply logic
accordingly.
### Example
```ts
importExportPlugin({
format: 'json',
})
Just spent an entire hour trying to figure out why my environment
variables are `undefined` on the client. Turns out, when running `pnpm
next build --experimental-build-mode compile`, it skips the environment
variable inlining step.
This adds a new section to the docs mentioning that you can use `pnpm
next build --experimental-build-mode generate-env` to manually inline
them.
### What?
Adds support for two new plugin options in the import-export plugin:
- `disableSave`: disables the "Save" button in the export UI view.
- `disableDownload`: disables the "Download" button in the export UI
view.
### Why?
This allows implementers to control user access to export actions based
on context or feature requirements. For example, some use cases may want
to preview an export without saving it, or disable downloads entirely.
### How?
- Injected `disableSave` and `disableDownload` into `admin.custom` for
the `exports` collection.
- Updated the `ExportSaveButton` component to conditionally render each
button based on the injected flags.
- Defaults to `false` if the values are not explicitly set in
`pluginConfig`.
### Example
```ts
importExportPlugin({
disableSave: true, // Defaults to false
disableDownload: true, // Defaults to false
})
Fixes the post-release-templates workflow by building payload before
running the gen templates script
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210784057163370
The selector could become hidden by:
- logging in with a user that only has 1 tenant
- logging out
- logging in with a user that has more than 1 tenant
Simplifies useEffect usage. Adds e2e test for this case.
Disables add row button using disabled prop from useField - i.e. when
the form is processing or initializing.
This fixes a flaky array test that clicks the button before the form has
finished initializing/processing.
Also corrects the add row button color styles with specificity.
### What?
Prevents `buildFormStateHandler` from returning `null` in unauthorized
scenarios by throwing an explicit `Error` instead.
### Why?
The `BuildFormStateResult` type does not include `null`, but previously
the handler returned `null` when access was unauthorized. This caused
runtime type mismatches and forced client-side workarounds (e.g.
guarding destructures).
By always throwing instead of returning `null`, the client code can
safely assume a valid result or catch errors.
<img width="1772" height="723" alt="Screenshot_2025-07-10_185618"
src="https://github.com/user-attachments/assets/d65344e3-a2cb-4ec5-91bf-a353b5b7dd14"
/>
### How?
- Replaced the `return null` with `throw new Error('Unauthorized')` in
`buildFormStateHandler`.
- Client code no longer needs to handle `null` responses from
`getFormState`.
### What
Introduces an additional `mimeType` validation based on the actual file
data to ensure the uploaded file matches the allowed `mimeTypes` defined
in the upload config.
### Why?
The current validation relies on the file extension, which can be easily
manipulated. For example, if only PDFs are allowed, a JPEG renamed to
`image.pdf` would bypass the check and be accepted. This change prevents
such cases by verifying the true MIME type.
### How?
Performs a secondary validation using the file’s binary data (buffer),
providing a more reliable MIME type check.
Fixes#12905
Monomorphic join fields were not using the `draft` argument when
fetching documents to display in the table. This change makes the join
field treatment of drafts consistent with the `relationship` type
fields.
Added e2e test to cover.
### What?
Updated the `FieldsToExport` component to use the current list view
query (`query.columns`) instead of saved preferences to determine which
fields to export.
### Why?
Previously, the export field selection was based on collection
preferences, which are only updated on page reload. This caused stale or
incorrect field sets to be exported if the user changed visible columns
without refreshing.
### How?
- Replaced `getPreference` usage with `useListQuery` to access
`query.columns`
- Filtered out excluded fields (those prefixed with `-`) to get only the
visible columns
- Fallbacks to `defaultColumns` if `query.columns` is not available
### What?
Added a delayed toast message to indicate when an export is being
processed, and disabled the download button unless the export form has
been modified.
### Why?
Previously, there was no feedback during longer export operations, which
could confuse users if the request took time to complete. Also, the
download button was always enabled, even when the form had not been
modified — which could lead to unnecessary exports.
### How?
- Introduced a 200ms delay before showing a "Your export is being
processed..." toast
- Automatically dismisses the toast once the download completes or fails
- Hooked into `useFormModified` to:
- Track whether the export form has been changed
- Disable the download button when the form is unmodified
- Reset the modified state after triggering a download
Previously you could've selected a date and time in the past to schedule
publish.
Now we ensure that there is a minimum time and date for scheduled
publishing date picker.
Additionally updated the disabled items to be more visually obvious that
they are disabled:
<img width="404" height="336" alt="image"
src="https://github.com/user-attachments/assets/1f4ea36a-267e-4ae5-91e4-92bb84d7889c"
/>
While we can use `joins`, `select`, `populate`, `depth` or `draft` on
auth collections when finding or finding by ID, these arguments weren't
supported for `/me` which meant that in some situations like in our
ecommerce template we couldn't optimise these calls.
A workaround would be to make a call to `/me` and then get the user ID
to then use for a `findByID` operation.
The login operation with sessions enabled calls updateOne, in mongodb,
data that does not match the schema is removed. `collection` and
`_strategy` are not part of the schema so they need to be reassigned
after the user is updated.
Adds int test.
This PR makes it so that `modifyResponseHeaders` is supported in our
adapters when set on the collection config. Previously it would be
ignored.
This means that users can now modify or append new headers to what's
returned by each service.
```ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: {
modifyResponseHeaders: ({ headers }) => {
const newHeaders = new Headers(headers) // Copy existing headers
newHeaders.set('X-Frame-Options', 'DENY') // Set new header
return newHeaders
},
},
}
```
Also adds support for `void` return on the `modifyResponseHeaders`
function in the case where the user just wants to use existing headers
and doesn't need more control.
eg:
```ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: {
modifyResponseHeaders: ({ headers }) => {
headers.set('X-Frame-Options', 'DENY') // You can directly set headers without returning
},
},
}
```
Manual testing checklist (no CI e2es setup for these envs yet):
- [x] GCS
- [x] S3
- [x] Azure
- [x] UploadThing
- [x] Vercel Blob
---------
Co-authored-by: James <james@trbl.design>
In case, if `payload.db.updateOne` received simple data, meaning no:
* Arrays / Blocks
* Localized Fields
* `hasMany: true` text / select / number / relationship fields
* relationship fields with `relationTo` as an array
This PR simplifies the logic to a single SQL `set` call. No any extra
(useless) steps with rewriting all the arrays / blocks / localized
tables even if there were no any changes to them. However, it's good to
note that `payload.update` (not `payload.db.updateOne`) as for now
passes all the previous data as well, so this change won't have any
effect unless you're using `payload.db.updateOne` directly (or for our
internal logic that uses it), in the future a separate PR with
optimization for `payload.update` as well may be implemented.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210710489889576
Currently, when a nonexistent document is accessed via the URL, a
`NotFound` page is displayed with a button to return to the dashboard.
In most cases, the next step the user will take is to navigate to the
list of documents in that collection. If we automatically redirect users
to the list view and display the error in a banner, we can save them a
couple of redirects.
This is a very common scenario when writing tests or restarting the
local environment.
## Before

## After

### What?
Improves the flattening logic used in the import-export plugin to
correctly handle polymorphic relationships (both `hasOne` and `hasMany`)
when generating CSV columns.
### Why?
Previously, `hasMany` polymorphic relationships would flatten their full
`value` object recursively, resulting in unwanted keys like `createdAt`,
`title`, `email`, etc. This change ensures that only the `id` and
`relationTo` fields are included, matching how `hasOne` polymorphic
fields already behave.
### How?
- Updated `flattenObject` to special-case `hasMany` polymorphic
relationships and extract only `relationTo` and `id` per index.
- Refined `getFlattenedFieldKeys` to return correct column keys for
polymorphic fields:
- `hasMany polymorphic → name_0_relationTo`, `name_0_id`
- `hasOne polymorphic → name_relationTo`, `name_id`
- `monomorphic → name` or `name_0`
- **Added try/catch blocks** around `toCSVFunctions` calls in
`flattenObject`, with descriptive error messages including the column
path and input value. This improves debuggability if a custom `toCSV`
function throws.
### What?
Updated the `selectionToUse` export field to properly render a radio
group with dynamic options based on current selection state and applied
filters.
- Fixed an edge case where `currentFilters` would appear as an option
even when the `where` clause was empty (e.g. `{ or: [] }`).
### Why?
Previously, the `selectionToUse` field displayed all options (current
selection, current filters, all documents) regardless of context. This
caused confusion when only one of them was applicable.
### How?
- Added a custom field component that dynamically computes available
options based on:
- Current filters from `useListQuery`
- Selection state from `useSelection`
- Injected the dynamic `field` prop into `RadioGroupField` to enable
rendering.
- Ensured the `where` field updates automatically in sync with the
selected radio.
- Added `isWhereEmpty` utility to avoid showing `currentFilters` when
`query.where` contains no meaningful conditions (e.g. `{ or: [] }`).
Previously, we were performing this check before calling the fetch
function. This changes it to perform the check within the dispatcher.
It adjusts the int tests to both trigger the dispatcher lookup function
(which is only triggered when not already passing a valid IP) and the
check before calling fetch
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210733180484570
### What?
Fixes a sync issue between the "Fields to Export" `<ReactSelect />`
dropdown and the underlying form state in the import-export plugin.
### Why?
Previously, the dropdown displayed outdated selections until an extra
click occurred. This was caused by an unnecessary `useState`
(`displayedValue`) that fell out of sync with the `useField` form value.
### How?
- Removed the separate `displayedValue` state
- Derived the selected values directly from the form field value using
inline mapping
Occasionally, I find myself on a URL like
`https://domain.com/admin/collections/myCollection/docId` and I modify
the URL with the intention of going to the admin panel, but I shorten it
in the wrong place: `https://domain.com/admin/collections`.
The confusion arises because the admin panel basically displays the
collections.
I think this redirect is a subtle but nice touch, since `/collections`
is a URL that doesn't exist.
EDIT: now I'm doing also the same thing for `/globals`
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
This PR introduces support for copy + pasting complex fields such as
Arrays and Blocks. These changes introduce a new `ClipboardAction`
component that houses logic for copy + pasting to and from the clipboard
to supported fields. I've scoped this PR to include only Blocks &
Arrays, however the structure of the components introduced lend
themselves to be easily extended to other field types. I've limited the
scope because there may be design & functional blockers that make it
unclear how to add actions to particular fields.
Supported fields:
- Arrays
([Demo](https://github.com/user-attachments/assets/523916f6-77d0-43e2-9a11-a6a9d8c1b71c))
- Array Rows
([Demo](https://github.com/user-attachments/assets/0cd01a1f-3e5e-4fea-ac83-8c0bba8d1aac))
- Blocks
([Demo](https://github.com/user-attachments/assets/4c55ac2b-55f4-4793-9b53-309b2e090dd9))
- Block Rows
([Demo](https://github.com/user-attachments/assets/1b4d2bea-981a-485b-a6c4-c59a77a50567))
Fields that may be supported in the future with minimal effort by
adopting the changes introduced here:
- Tabs
- Groups
- Collapsible
- Relationships
This PR also encompasses e2e tests that check both field and row-level
copy/pasting.
### Why?
To make it simpler and faster to copy complex fields over between
documents and rows within those docs.
### How?
Introduces a new `ClipboardAction` component with helper utilities to
aid in copy/pasting and validating field data.
Addresses #2977 & #10703
Notes:
- There seems to be an issue with Blocks & Arrays that contain RichText
fields where the RichText field dissappears from the dom upon replacing
form state. These fields are resurfaced after either saving the data or
dragging/dropping the row containing them.
- Copying a Row and then pasting it at the field-level will overwrite
the field to include only that one row. This is intended however can be
changed if requested.
- Clipboard permissions are required to use this feature. [See Clipboard
API caniuse](https://caniuse.com/async-clipboard).
#### TODO
- [x] ~~I forgot BlockReferences~~
- [x] ~~Fix tests failing due to new buttons causing locator conflicts~~
- [x] ~~Ensure deeply nested structures work~~
- [x] ~~Add missing translations~~
- [x] ~~Implement local storage instead of clipboard api~~
- [x] ~~Improve tests~~
---------
Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
Parse `.tool-versions` file in the composite node setup action. This
will make it the source of truth and easier to bump node/pnpm versions
in the future.
Fixes#12834
`loginAttempts` was being shown in the admin panel when it should be
hidden. The field is set to `hidden: true` therefore the value is
removed from siblingData and passes the `allowDefaultValue` check -
showing inconsistent data.
This PR ensures the default value is not returned if the field has a
value but was removed due to the field being hidden.
Login attempts were not being reset correctly which led to situations
where a failed login attempt followed by a successful login attempt
would keep the loginAttempts at 1.
### Before
Example with maxAttempts of 2:
- failed login -> `loginAttempts: 1`
- successful login -> `loginAttempts: 1`
- failed login -> `loginAttempts: 2`
- successful login -> `"This user is locked due to having too many
failed login attempts."`
### After
Example with maxAttempts of 2:
- failed login -> `loginAttempts: 1`
- successful login -> `loginAttempts: 0`
- failed login -> `loginAttempts: 1`
- successful login -> `loginAttempts: 0`
Ensures Live Preview url functions aren't fired during create or on
collections that do not have Live Preview enabled.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210743577153852
### What?
Adds a new `sanitizeUserDataForEmail` function, exported from
`payload/shared`.
This function sanitizes user data passed to email templates to prevent
injection of HTML, executable code, or other malicious content.
### Why?
In the existing `email` example, we directly insert `user.name` into the
generated email content. Similarly, the `newsletter` collection uses
`doc.name` directly in the email content. A security report identified
this as a potential vulnerability that could be exploited and used to
inject executable or malicious code.
Although this issue does not originate from Payload core, developers
using our examples may unknowingly introduce this vulnerability into
their own codebases.
### How?
Introduces the pre-built `sanitizeUserDataForEmail` function and updates
relevant email examples to use it.
**Fixes `CMS2-1225-14`**
### What?
- Updates the `RenderTitle` component to check that the `title` is a
string before returning it.
- Adds note to docs that **Relationship** and **Join** fields cannot be
assigned to `useAsTitle`, a **virtual** field should be used instead.
### Why?
When autosave is enabled and the `useAsTitle` points to a relationship
field, the autosave process returns an `object` for the title, this gets
passed to the `RenderTitle` component and throws an error which crashes
the UI.
### How?
Safely checks that `title` is a string before rendering it in
`RenderTitle` and updates docs to clarify that Relationship/Joins are
not compatible with `useAsTitle`.
Fixes#12960
## Summary
Fixed a grammatical error in the README files for the website templates.
## Changes
- Fixed grammar in the on-demand revalidation section: changed "or
footer or header, change they will" to "footer, or header changes will"
## Files Changed
- `templates/website/README.md`
- `templates/with-vercel-website/README.md`
## Type of Change
- [x] Documentation fix/improvement
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
This fixes a typo that was making the sentence grammatically incorrect
and hard to read.
When adding a custom ID field to an array's config, both the default
field provided by Payload, and the custom ID field, exist in the
resulting config. This can lead to problems when the looking up the
field's config, where either one or the other will be returned.
Fixes#12978
Adds `restrictedFileTypes` (default: `false`) to upload collections
which prevents files on a restricted list from being uploaded.
To skip this check:
- set `[Collection].upload.restrictedFileTypes` to `true`
- set `[Collection].upload.mimeType` to any type(s)
This adds a new `analyze` step to our CI that analyzes the bundle size
for our `payload`, `@payloadcms/ui`, `@payloadcms/next` and
`@payloadcms/richtext-lexical` packages.
It does so using a new `build:bundle-for-analysis` script that packages
can add if the normal build step does not output an esbuild-bundled
version suitable for analyzing. For example, `ui` already runs esbuild,
but we run it again using `build:bundle-for-analysis` because we do not
want to split the bundle.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210692087147570
### What?
Adds `token` into the `initialState` for the reset password form to
ensure `token` does not get passed through as `undefined`.
### Why?
Currently the reset password UI is broken because `token` is not getting
passed to the form state.
### How?
Adds `token` to `initialState`
Fixes#13040
Fixes https://github.com/payloadcms/payload/issues/13045
`updateOne` when returning is `false` mutates the data object for write
operations on the DB, but that causes an issue when using that data
object later on since all of the id's are mutated to objectIDs and never
transformed back into read id's.
This fix ensures that the transform happens even when the result is not
returned.
As stated in #12529, the setTimeout was defined through trial and error
as it wasn't possible to reproduce the bug with the devtools open and
therefore with the CPU throttled. One user reported still experiencing
the bug.
I'm increasing the timeout to 100ms, which seems acceptable enough to
keep postponing a better fix, considering the bug isn't that critical.
If we find it keeps happening, we'll probably need to investigate the
root cause.
### What?
This PR updates the `from` field in `plugin-redirects` to add `unique:
true`.
### Why?
If you create multiple redirects with the same `from` URL — the
application won't know which one to follow, which causes errors and
unpredictable behavior.
### How?
Adds `unique: true` to the plugin injected `from` field.
### Migration Required
This change will require a migration. Projects already using this plugin
will need to:
- Ensure there are no duplicate `from` values in their existing
redirects collection.
- Remove or modify any duplicate entries before applying this update.
Fixes#12959
Adds:
```ts
import { lookup } from 'dns/promises'
// ...
const { address } = await lookup(hostname)
// ...
return isSafeIp(address)
```
To ensure that an `ip` address is being verified. Previously, hostnames
were being verified by `isSafeIp`.
Fixes: https://github.com/payloadcms/payload/issues/12876
### What?
Fixes an issue where only the fields from the first batch of documents
were used to generate CSV column headers during streaming exports.
### Why?
Previously, columns were determined during the first streaming batch. If
a field appeared only in later documents, it was omitted from the CSV
entirely — leading to incomplete exports when fields were sparsely
populated across the dataset.
### How?
- Adds a **pre-scan step** before streaming begins to collect all column
keys across all pages
- Uses this superset of keys to define the final CSV header
- Ensures every row is padded to match the full column set
This matches the behavior of non-streamed exports and guarantees that
the streamed CSV output includes all relevant fields, regardless of when
they appear in pagination.
This fixes the payload bundle script. While not run by default, it's
useful for checking the payload bundle size by manually running `cd
packages/payload && node bundle.js`.
Previously, `db.updateOne` calls with `where` queries that lead to no
results would create new rows on drizzle. Essentially, `db.updateOne`
behaved like `db.upsertOne` on drizzle
Removing the `setTimeout` not only doesn't break any tests, but it also
fixes the linked issue.
The long comment above the if statement was added in
https://github.com/payloadcms/payload/pull/5460 and explains why the if
statement is necessary GIVEN the existence of the `setTimeout`, but the
`setTimeout` was introduced [earlier because the button apparently
didn't work](https://github.com/payloadcms/payload/issues/1414).
It seems to work now without the `setTimeout`, because otherwise the
tests wouldn't even pass. I also tested it manually, and it works fine.
Fixes#12687
### What?
Shows error state (red left border) on small screens.
### Why?
The current error state disappears at small-break screen width.
### How?
Updates small-break error state to match the desktop error state for the
Lexical field.
##### Reported by client.
Required for #13005.
Opening an autosave-enabled document within a drawer triggers an
infinite loop when the root document is also autosave-enabled.
This was for two reasons:
1. Autosave would run and change the `updatedAt` timestamp. This would
trigger another run of autosave, and so on. The timestamp is now removed
before comparison to ensure that sequential autosave runs are skipped.
2. The `dequal()` call was not being given the `.current` property off
the ref object. This meant that is was never evaluate to `true` and
therefore never skip unnecessary autosaves to begin with.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210697235723932
### What?
Ensure the export preview table includes all field keys as columns, even
if those fields are not populated in any of the returned documents.
### Why?
Previously, if none of the documents in the preview result had a value
for a given field, that column would be missing entirely from the
preview table.
### How?
- Introduced a `getFlattenedFieldKeys` utility that recursively extracts
all missing flattened field accessors from the collection’s config that
are undefined
- Updates the preview UI logic to build columns from all flattened keys,
not just the first document
Adds support for `halfvec` and `sparsevec` and `bit` (binary vector)
column types. This is required for supporting indexing of embeddings >
2000 dimensions on postgres using the pg-vector extension.
### What?
This PR updates the `create` access control on the `users` collection in
the `multi-tenant` example to prevent unauthorized creation of
`super-admin` users.
### Why?
Previously, any authenticated user could create a new user and assign
them the `super-admin` role — even if they didn’t have that role
themselves. This bypassed role-based restrictions and introduced a
security vulnerability, allowing users to escalate their own privileges
by working around role restrictions during user creation.
### How?
The `create` access function now checks whether the current user has the
`super-admin` role before allowing the creation of another
`super-admin`. If not, the request is denied.
**Fixes:** `CMS2-Q225-01`
### What
This PR updates the `create` access control functions in the
`multi-tenant` example to ensure that any `tenant` specified in a create
request matches a tenant the user has admin access to.
### Why
Previously, while the admin panel UI restricted the tenant selection, it
was still possible to bypass this by making a request directly to the
API with a different `tenant`. This allowed users to create documents
under tenants they shouldn't have access to.
### How
The `access` functions on the `users` and `pages` collections now
explicitly check whether the tenant(s) in the request are included in
the user's tenant permissions. If not, access is denied by returning
`false`.
**Fixes: CMS2-Q225-03**
Supersedes #12992. Partially closes#12975.
Right now autosave-enabled documents opened within a drawer will
unnecessarily remount on every autosave interval, causing loss of input
focus, etc. This makes it nearly impossible to edit these documents,
especially if the interval is very short.
But the same is true for non-autosave documents when "manually" saving,
e.g. pressing the "save draft" or "publish changes" buttons. This has
gone largely unnoticed, however, as the user has already lost focus of
the form to interact with these controls, and they somewhat expect this
behavior or at least accept it.
Now, the form remains mounted across autosave events and the user's
cursor never loses focus. Much better.
Before:
https://github.com/user-attachments/assets/a159cdc0-21e8-45f6-a14d-6256e53bc3df
After:
https://github.com/user-attachments/assets/cd697439-1cd3-4033-8330-a5642f7810e8
Related: #12842
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210689077645986
### What?
Fixes a bug where adding an additional OR filter condition in the list
view selects a field with `admin.disableListFilter: true`, causing all
filter fields to appear disabled.
### Why?
When the first field in a collection has `disableListFilter` set to
`true`, adding a second OR condition defaults to using that field. This
leads to a broken filter UI where no valid fields are selectable.
### How?
Replaces the hardcoded usage of `reducedFields[0]` with a call to
`reducedFields.find(...) `that skips fields with `disableListFilter:
true`, consistent with the logic already used when adding the first
filter condition.
Fixes#12993
### What?
The "Preview Sizes" button in the file upload UI was not showing up if:
- `crop` and `focalPoint` were both `false`
- No `customUploadActions` were provided
- But image sizes were configured
### Why?
This happened because `UploadActions` wasn’t rendered at all unless
adjustments or custom actions were present.
### How?
Update the conditional in `StaticFileDetails` to also render
`UploadActions` when:
- `hasImageSizes` is `true` and the document has a `filename`
Fixes#12832
When using pnpm 10 to install any of our templates, the following
warning is thrown:

> Warning: Ignored build scripts: esbuild, unrs-resolver. Run "pnpm
approve-builds" to pick which dependencies should be allowed to run
scripts.
This PR fixes this by adding those packages to `onlyBuiltDependencies`
This PR consists of two separate changes. One change cannot pass CI
without the other, so both are included in this single PR.
## CI - ensure types are generated
Our website template is currently failing to build due to a type error.
This error was introduced by a change in our generated types.
Our CI did not catch this issue because it wasn't generating types /
import map before attempting to build the templates. This PR updates the
CI to generate types first.
It also updates some CI step names for improved clarity.
## Fix: type error

This fixes the type error by ensuring we consistently use the _same_
generated `TypedUser` object within payload, instead of `BaseUser`.
Previously, we sometimes used the generated-types user and sometimes the
base user, which was causing type conflicts depending on what the
generated user type was.
It also deprecates the `User` type (which was essentially just
`BaseUser`), as consumers should use `TypedUser` instead. `TypedUser`
will automatically fall back to `BaseUser` if no generated types exists,
but will accept passing it a generated-types User.
Without this change, additional properties added to the user via
generated-types may cause the user object to not be accepted by
functions that only accept a `User` instead of a `TypedUser`, which is
what failed here.
## Templates: re-generate templates to update generated types
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210668927737258
Fixes#12826
Leave without saving was being triggered when no changes were made to
the tenant. This should only happen if the value in form state differs
from that of the selected tenant, i.e. after changing tenants.
Adds tenant selector syncing so the selector updates when a tenant is
added or the name is edited.
Also adds E2E for most multi-tenant admin functionality.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210562742356842
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
This PR addresses an issue where the order of operations/conditions for
throwing an unverified email error were incorrect.
### Why?
To properly throw an unverified email error under the correct
conditions.
### How?
Pushing this error to be thrown later in the operation.
Mounts live preview to `../:id` instead `../:id/preview`.
This is a huge win for both UX and a maintainability standpoint.
Here are just a few of those wins:
1. If you edit a document, _then_ decide you want to preview those
changes, you are currently presented with the `LeaveWithoutSaving` modal
and are forced to either save your edits or clear them. This is because
you are being navigated to an entirely new page with it's own form
context. Instead, you should be able to freely navigate back and forth
between the two.
2. If you are an editor who most often uses Live Preview, or you are
editing a collection that typically requires it, you likely want it to
automatically enter live preview mode when you open a document.
Currently, the user has to navigate to the document _first_, then use
the live preview tab. Instead, you should be able to set a preference
and avoid this extra step.
3. Since the inception of Live Preview, we've been maintaining largely
the same code across the default edit view and the live preview view,
which often became out of sync and inconsistent—but they're essentially
doing the same thing. While we could abstract a lot of this out, it is
no longer necessary if the two views are combined into one.
This change does also include some small modifications to UI. The "Live
Preview" tab no longer exists, and instead has been replaced with a
button placed next to the document controls (subject to change).
Before:
https://github.com/user-attachments/assets/48518b02-87ba-4750-ba7b-b21b5c75240a
After:
https://github.com/user-attachments/assets/a8ec8657-a6d6-4ee1-b9a7-3c1173bcfa96
### What?
Adds optional chaining when accessing `rows` in `mergeServerFormState`
to prevent error crashing the UI.
### Why?
When an array field is populated in a `beforeChange` hook and was
previously empty, it crashes `mergeServerFormState.ts` on this line
because no `rows` exist:
```ts
const indexInCurrentState = currentState[path].rows.findIndex
```
The line after this checks `if (indexInCurrentState > -1)` so returning
undefined here will not affect the subsequent code.
### How?
Added optional chaining to the access of `rows`, which prevents the
error being thrown.
Fixes#12944
Adds full session functionality into Payload's existing local
authentication strategy.
It's enabled by default, because this is a more secure pattern that we
should enforce. However, we have provided an opt-out pattern for those
that want to stick to stateless JWT authentication by passing
`collectionConfig.auth.useSessions: false`.
Todo:
- [x] @jessrynkar to update the Next.js server functions for refresh and
logout to support these new features
- [x] @jessrynkar resolve build errors
---------
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
Co-authored-by: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com>
Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
Needed for #12860.
The new live preview pattern requires collection-level preferences, a
pattern that does not yet exist.
Instead of creating a new record for these types of preferences, we can
simply reuse `<collectionSlug>-list` under a more general key:
`collection-<slug>`. This way other relevant properties can be attached
in the future that might not specifically apply to the list view.
This will also match the conventions already estalished by
document-level preferences in `collection-<slug>-<id>` and
`global-<slug>`.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210628212784050
This pull request updates the `Card` component in the localization
example to support localized URLs. The most significant changes include
importing a new hook for locale management and modifying the URL
generation logic to include the locale.
Localization updates:
*
[`examples/localization/src/components/Card/index.tsx`](diffhunk://#diff-619212c47638e7ff51284c62740ba188c87f008d481442b7f4951e2c150a2415R5):
Imported `useLocale` from `next-intl` to manage locale-based
functionality.
*
[`examples/localization/src/components/Card/index.tsx`](diffhunk://#diff-619212c47638e7ff51284c62740ba188c87f008d481442b7f4951e2c150a2415R20):
Added a `locale` constant using the `useLocale` hook to retrieve the
current locale.
*
[`examples/localization/src/components/Card/index.tsx`](diffhunk://#diff-619212c47638e7ff51284c62740ba188c87f008d481442b7f4951e2c150a2415L28-R30):
Updated the `href` generation logic to include the locale in the URL
structure, ensuring localized navigation.
When using 3rd party custom components in an edit form there exists a
possibility that a non-navigational click event will propagate through
to payload.
In this case the `findClosestAnchor` function in `usePreventLeave` may
find an anchor without href, resulting in the `newUrlObj = new
URL(newUrl)` in `isAnchorOfCurrentUrl` throwing the exception:
> TypeError: URL constructor: is not a valid URL.
As a result a native alert is shown to the user, with no real
explanation as to what is going on. This is not a good experience.
I suggest moving it to a console log which is less "in your face" for
users who do not know what to do about it anyway.
I discovered this while using a data grid component with a context menu.
Clicking on menu items (which are `<a>` tags without href in this
component) triggers the error.
(Another on-liner fix would ofc be to not attempt to create an URL
object if there is no href `if (anchor?.href) {`, but I opted for this
version since using `alert()` in production code is not a preferred
practice anyway)
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
This fixes a small ui bug where the items in the table header were not
vertically aligned when they don't contain the SortColumn component. The
SortColumn component handles vertical alignment with a nested flexbox.
The PR adds vertical-align: middle directly to the th element so that
the text in the header is vertically aligned even when there isn't a
nested flexbox
Before:
<img width="719" alt="Screenshot 2025-06-05 at 10 24 19 AM"
src="https://github.com/user-attachments/assets/3962517e-3b22-452a-af04-8397549c4ed9"
/>
After:
<img width="719" alt="Screenshot 2025-06-05 at 10 30 39 AM"
src="https://github.com/user-attachments/assets/0c5a0847-8ee2-4439-981e-f3538908e920"
/>
### What?
Updates the tenant selector displayed in the sidebar when a new tenant
is created.
### Why?
Currently when using the multi-tenant plugin and creating a new tenant
doc, the tenant selector dropdown does not display the new tenant as an
option until the page gets refreshed.
### How?
Extends the `WatchTenantCollection` helper to check if the tenant `id`
from the current doc exists, if the tenant is new it manually calls
`updateTenants`. The `updateTenants` function previously only adjusted
the title on existing tenants, this has been updated to add a new tenant
as an option when it doesn't exist.
#### Reported by client
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
This PR fixes an issue where the bottom "Create new ..." button would
cause a runtime error due to not accounting for a polymorphic join
setup.
### Why?
To prevent a runtime error and allow users the ability to add new
documents to the join as expected even in a polymorphic setup.
### How?
Creation of a new `AddNewButton` which handles all of the add new button
instances in the `RelationshipTable` component.
Addresses
https://github.com/payloadcms/payload/issues/12913#issuecomment-3001475438
Before:
[join-polymorphic-runtime-error--Payload.webm](https://github.com/user-attachments/assets/fad3a1ba-c51c-4731-84cc-c27adbaac1d9)
After:
[polymorphic-after-Editing---Multiple-Collections-Parent---Payload
(1).webm](https://github.com/user-attachments/assets/e3baf902-1b2b-4f19-8b6d-838edd6fef80)
## What / Why
Date & Time fields were rendering their field label as a `<span>` while
every other field type uses a proper `<label>` with a matching
`htmlFor`.
Because the element was a span it broke styles and made 'field-label'
have different styles from the rest of 'field-label's.
**Root cause:** DateTimeField failed to pass its `path` (or an explicit
`htmlFor`) to `FieldLabel`. When `FieldLabel` receives no `htmlFor`, it
intentionally downgrades to a `<span>`.
## Screenshots
### Before

*DateTime label rendered as `<span>`, causing style inconsistencies*
### After

*DateTime label now rendered as proper `<label>` element*
## Changes introduced
- `packages/ui/src/fields/DateTime/index.tsx`
- Added `path={path}` prop to `FieldLabel` component
## Behavior after the fix
- Date-time labels are now real `<label>` elements with `for="field-…"`
- Visual alignment now matches every other field type
## How to test manually
1. Run `pnpm dev fields`
2. Inspect the DateTime field markup – label is now `<label>`
3. Observe that vertical spacing matches other types of fields
If you (using the MongoDB adapter) delete a block from the payload
config, but still have some data with that block in the DB, you'd
receive in the admin panel an error like:
```
Block with type "cta" was found in block data, but no block with that type is defined in the config for field with schema path pages.blocks
```
Now, we remove those "unknown" blocks at the DB adapter level.
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
### What
This PR updates the import-export plugin's `<Preview />` component to
render table columns and rows using the same logic as the CSV export.
Key changes:
- Adds a new `/api/preview-data` custom REST endpoint that:
- Accepts filters (`fields`, `where`, `sort`, `draft`, `limit`)
- Uses `getCustomFieldFunctions` and `flattenObject` to transform
documents
- Returns deeply flattened rows identical to the CSV export
- Refactors the <Preview /> component to:
- POST preview config to the new endpoint instead of querying the
collection directly
- Match column ordering and flattening logic with the `createExport`
function
- Ensures consistency across CSV downloads and in-admin previews
-Adds JSON preview
This ensures preview results now exactly match exported CSV content,
including support for custom field transformers and polymorphic fields.
---------
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
### What?
This PR solves an issue with validation of the `point` field in Payload
CMS. If the value is `null` and the field is not required, the
validation will return `true` before trying to examine the contents of
the field
### Why?
If the point field is given a value, and saved, it is then impossible to
successfully "unset" the point field, either through the CMS UI or
through a hook like `beforeChange`. Trying to do so will throw this
error:
```
[17:09:41] ERROR: Cannot read properties of null (reading '0')
err: {
"type": "TypeError",
"message": "Cannot read properties of null (reading '0')",
"stack":
TypeError: Cannot read properties of null (reading '0')
at point (webpack-internal:///(rsc)/./node_modules/.pnpm/payload@3.43.0_graphql@16.10.0_typescript@5.7.3/node_modules/payload/dist/fields/validations.js:622:40)
```
because a value of `null` will not be changed to the default value of
`['','']`, which in any case does not pass MongoDB validation either.
```
[17:22:49] ERROR: Cast to [Number] failed for value "[ NaN, NaN ]" (type string) at path "location.coordinates.0" because of "CastError"
err: {
"type": "CastError",
"message": "Cast to [Number] failed for value \"[ NaN, NaN ]\" (type string) at path \"location.coordinates.0\" because of \"CastError\"",
"stack":
CastError: Cast to [Number] failed for value "[ NaN, NaN ]" (type string) at path "location.coordinates.0" because of "CastError"
at SchemaArray.cast (webpack-internal:///(rsc)/./node_modules/.pnpm/mongoose@8.15.1_@aws-sdk+credential-providers@3.778.0/node_modules/mongoose/lib/schema/array.js:414:15)
```
### How?
This adds a check to the top of the `point` validation function and
returns early before trying to examine the contents of the point field
---------
Co-authored-by: Dave Ryan <dmr@Daves-MacBook-Pro.local>
https://github.com/payloadcms/payload/pull/12861 introduced some flaky
test selectors. Specifically bulk editing values and then looking for
the previous values in the table rows.
This PR fixes the flakes and fixes eslint errors in `findTableRow` and
`findTableCell` helper funcitons.
### What?
Set the `limit` query param on API requests called within the
`useLivePreview` hook.
### Why?
We are heavily relying on the block system in our pages and we reuse the
media collection in a lot of the block types. When the page has more
than 10 images, the API request doesn't fetch all of them for live
preview due to the default 10 item `limit`. This PR allows the preview
page to override this `limit` so that all the items get correctly
fetched.
### Our current workaround
Set the `depth` param of `useLivePreview` hook like this:
```
useLivePreview({
// ...
depth: '1000&limit=1000',
})
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210643905956939
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Needed for #12860.
If the admin panel broadcasts foreign postMessage events, i.e. those
without the `payload-live-preview` signature, client-side live preview
subscriptions will reset back to initial state.
This is because we dispatch two postMessage events in the admin panel,
one for client-side live preview to catch (`payload-live-preview`), and
the other for server-side live preview (`payload-document-event`). This
was not previously noticeable because both events would only get called
simultaneously on initial render, where initial state is already the
expected result.
Now that Live Preview can be freely toggled on and off, both events are
frequently dispatched and very obviously disregard the current working
state.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210628466702818
Needed for #12860.
The client config unnecessarily omits the `livePreview.collections` and
`livePreview.globals` properties. This is because the root live preview
config extends the type with these two additional properties without
sharing it elsewhere. To led to the client sanitization function
overlooking these additional properties, as there was no type indication
that they exist.
The `collections` and `globals` properties are now appended to the
client config as expected, and the root live preview is standardized
behind the `RootLivePreviewConfig` type to ensure no properties are
lost.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210628466702823
This PR adds int tests with vitest and e2e tests with playwright
directly into our templates.
The following are also updated:
- bumps core turbo to 2.5.4 in monorepo
- blank and website templates moved up to be part of the monorepo
workspace
- this means we now have thes templates filtered out in pnpm commands in
package.json
- they will now by default use workspace packages which we can use for
manual testing and int and e2e tests
- note that turbo doesnt work with these for dev in monorepo context
- CPA script will fetch latest version and then replace `workspace:*` or
the pinned version in the package.json before installation
- blank template no longer uses _template as a base, this is to simplify
management for workspace
- updated the generate template variations script
Partially closes#12121.
When you edit a document in Live Preview using the default iframe
window, then attempt to open the window as a popup, the
`LeaveWithoutSaving` modal will appear.
This is because the `usePreventLeave` hook watches for anchor tags that
might cause a page navigation, and rightfully warns the user before they
navigate away and lose their changes. The reason the popup button
triggers this hook is because it uses an anchor tag with an href for
accessibility, which fires events that are caught and processed by the
hook.
The fix is to add the `target="_blank"` attribute here so that the hook
understands that these events do not navigate the user away from the
page and can be ignored.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210643905956946
### What?
Fixes#12811
### Why?
Custom Views become unreachable when admin route is set to "/" because
the forward slash of the current route gets removed before routing to
custom view
### How?
Fixes #
-->
Fixes#12811
Custom Views become unreachable when admin route is set to "/" because
the forward slash of the current route gets removed before routing to
custom view
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210582760545830
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
This PR fixes an issue in the export logic where CSV downloads would
include duplicate rows and repeated column headers across paginated
batches.
Key changes:
- Ensured `page` is incremented correctly after each `payload.find` call
- Tracked and wrote CSV column headers only once for the first page
- Prevented row duplication by removing unused `result` initialization
and using isolated `page` tracking
- Streamlined both download and non-download logic for consistent batch
processing
This resolves incorrect row counts and header duplication in large CSV
exports.
We were running scripts as they were without encompassing our logic in a
function for jest's teardown and we were subsequently running
`process.exit(0)` which meant that tests didn't correctly return an
error status code when they failed in CI.
The following tests have been skipped as well:
```
● postgres vector custom column › should add a vector column and query it
● Sort › Local API › Orderable › should not break with existing base 62 digits
● Sort › Local API › Orderable join › should set order by default
● Sort › Local API › Orderable join › should allow setting the order with the local API
● Sort › Local API › Orderable join › should sort join docs in the correct
```
---------
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
### What?
Fixes a crash when exporting documents to CSV if a custom `toCSV`
function tries to access properties on a `null` value.
### Why?
In some cases (especially with Postgres), fields like relationships may
be explicitly `null` if unset. Custom `toCSV` functions that assume the
value is always defined would throw a `TypeError` when attempting to
access nested properties like `value.id`.
### How?
Added a null check in the custom `toCSV` implementation for
`customRelationship`, ensuring the field is an object before accessing
its properties.
This prevents the export from failing and makes custom field transforms
more resilient to missing or optional values.
### What?
Fixes CSV export support for polymorphic relationship and upload fields.
### Why?
Polymorphic fields in Payload use a `{ relationTo, value }` structure.
The previous implementation incorrectly accessed `.id` directly on the
top-level object, which caused issues depending on query depth or data
shape. This led to missing or invalid values in exported CSVs.
### How?
- Updated getCustomFieldFunctions to safely access relationTo and
value.id from polymorphic fields
- Ensured `hasMany` polymorphic fields export each related ID and
relationTo as separate CSV columns
### What?
Reflects any access control restrictions applied to Auth fields in the
UI. I.e. if `email` has `update: () => false` the field should be
displayed as read-only.
### Why?
Currently any access control that is applied to auth fields is
functional but is not matched within the UI.
For example:
- `password` that does not have read access will not return data, but
the field will still be shown when it should be hidden
- `email` that does not have update access, updating the field and
saving the doc will **not** update the data, but it should be displayed
as read-only so nothing can be filled out and the updating restriction
is made clear
### How?
Passes field permissions through to the Auth fields UI and adds docs
with instructions on how to override auth field access.
#### Testing
Use `access-control` test suite and `auth` collection. Tests added to
`access-control` e2e.
Fixes#11569
### What?
This fix prevents custom row labels being removed when duplicating array
items.
### Why?
Currently, when you have an array with custom row labels, if you create
a new array item by duplicating an existing item, the new item will have
no custom row label until you refresh the page.
### How?
During the `duplicate` process, we remove any react components from the
field state. This change intentionally re-adds the `RowLabel` if one
exists.
#### Reported by client
### What?
Ensure fields using a custom `toCSV` function that return `undefined`
are excluded from the exported CSV.
### Why?
Previously, when a `toCSV` function returned `undefined`, the field key
would still be added to the export row. This caused the column to appear
in the CSV output with an empty string value (`""`), leading to
unexpected results and failed assertions in tests expecting the field to
be truly omitted.
### How?
Updated the `flattenObject` utility to:
- Check if the value returned by a `toCSV` function is `undefined`
- Only assign the value to the export row if it is explicitly defined
- Applied this logic in all relevant paths (arrays, objects, primitives)
This change ensures that fields are only included in the CSV when a
meaningful value is returned.
Because of this check, if a JSON with a property `target` was saved it
would become malformed.
For example trying to save a JSON field:
```json
{
"target": {
"value": {
"foo": "bar"
}
}
}
```
would result in:
```json
{
"foo": "bar"
}
```
And trying to save:
```json
{
"target": "foo"
}
```
would just not save anything:
```json
null
```
I went through all of the field types and did not find a single one that
would rely on this ternary. Seems like it always defaulted to `const val
= e`, except the unexpected case described previously.
Fixes#12873
Added test may be overkill, will remove if so.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210628466702813
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
### What?
This PR fixes a runtime error that occurs when opening the "More
versions..." drawer while browsing the versions for a global. It also
fixes a minor runtime error when navigating to a global version view
where an optional chaining operator was missing as the collection
variable would be undefined as we are viewing a global.
This PR also adds an e2e test to ensure the versions drawer is
accessible and renders the appropriate number of versions for globals.
### Why?
To properly render global version views without errors.
### How?
By threading the global slug to the versions drawer and adjusting some
properties of the `renderDocument` server function call there. This PR
also adds an optional chaining operator the `versionUseAsTitle` in the
original view to prevent an error in globals.
Notes:
- This was brought to my attention in Discord by a handful of users
Before: (Missing optional chaining error)
[error1-verions-Editing---Menu---Payload.webm](https://github.com/user-attachments/assets/3dc4dbe4-ee5a-43df-8d25-05128b05e063)
Before: (Versions drawer error)
[error2-versions-Editing---Menu---Payload.webm](https://github.com/user-attachments/assets/98c3e1da-cb0b-4a36-bafd-240f641e8814)
After:
[versions-globals-Dashboard---Payload.webm](https://github.com/user-attachments/assets/c778d3f0-a8fe-4e31-92cb-62da8e6d8cb4)
Fixes an issue when querying deeply new relationship virtual fields with
`draft: true`. Changes the method for `where` sanitization, before it
was done in `validateSearchParam` which didn't work with versions
properly, now there's a separate `sanitizeWhereQuery` function that does
this.
This PR removes the `packages/payload/src/assets` folder for the
following reasons:
- they were published to npm. Removing this decreases the install size
of payload (excluding dependencies) from 6.22MB => 5.12MB
- most assets were unused. The only used ones were moved to a different
directory that does not get published to npm
This also updates some outdated asset URLs in our examples
The `@payloadcms/next/auth` functions are unnecessarily wrapped with
`try...catch` blocks that propagate the original error as a plain
string. This makes it impossible for the end user's error handling to
differentiate between error types.
These functions also throw errors regardless, and therefore must be
wrapped with proper error handling anyway. Especially after removing the
internal logging in #12881, these blocks do not serve any purpose.
This PR also removes unused imports.
### What?
Removes the console.error() statement when there is a login error.
### Why?
IMO, Libraries should not pollute the console with log statements in all
but the most exceptional cases. This prevents users of the library from
controlling what goes to standard out. For example, if I want to use
structured logging, this log line breaks it.
It would be a little better if this console.error() only executed on
unexpected errors, but it executes even when a user puts the wrong email
/ password, so it gets printed relatively frequently.
I think you can just remove the logging and let the user of this
function catch the error and log as they see fit.
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change -->
### What?
I found that the `devBundleServerPackages` parameter is not present in
the documentation because it was spelled as `sortOnOptions`.
### Why?
Cannot find `devBundleServerPackages` using vscode intellisense.
### How?
I simply changed back `sortOnOptions` to `options` in JSDoc comments.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
-->
### What?
Fixes anchor links leading to
[`filterOptions`](https://payloadcms.com/docs/fields/select#filteroptions)
### How?
Replaced camel case with lower case.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
-->
### What?
Hi Payload Team, this PR is a reply to @DanRibbens's request to document
#11478. Let me know if you'd like to see any changes - thank you!
This is teeny tiny – the sentence "Out of the box Payload ships with a
three powerful Authentication strategies:" has an unnecessary "a" on the
Authentication overview page. This PR removes it.
Fixes https://github.com/payloadcms/payload/issues/12847
- Uses rem instead of em for inline padding, for indent consistency
between nodes with different font sizes
- Use rem instead of px in deprecated html converters for consistency
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210564720112211
### What?
After creating a new document from a relationship field, this doc should
automatically become the selected document for that relationship field.
This is the expected and current behavior. However, when the
relationship ties to a collection with autosave enabled, this does not
happen.
### Why?
This is expected behavior and should still happen when the relationship
is using an autosave enabled collection.
### How?
1. The logic in `addNewRelation` contained an `if` statement that
checked for `operation === 'create'` - however when autosave is enabled,
the `create` operation runs on the first data update and subsequently it
is a `update` operation.
2. The `onSave` from the document drawer provider was not being run as
part of the autosave workflow.
#### Reported by client.
This simplifies workflow / task error handling, as well as cancelling
jobs. Previously, we were handling errors when they occur and passing
through error state using a `state` object - errors were then handled in
multiple areas of the code.
This PR adds new, clean `TaskError`, `WorkflowError` and
`JobCancelledError` errors that are thrown when they occur and are
handled **in one single place**, massively cleaning up complex functions
like
[payload/src/queues/operations/runJobs/runJob/getRunTaskFunction.ts](https://github.com/payloadcms/payload/compare/refactor/jobs-errors?expand=1#diff-53dc7ccb7c8e023c9ba63fdd2e78c32ad0be606a2c64a3512abad87893f5fd21)
Performance will also be positively improved by this change -
previously, as task / workflow failure or cancellation would have
resulted in multiple, separate `updateJob` db calls, as data
modifications to the job object required for storing failure state were
done multiple times in multiple areas of the codebase. Most notably,
task error state was handled and updated separately from workflow error
state.
Now, it's just a clean, single `updateJob` call
This PR also does the following:
- adds a new test for `deleteJobOnComplete` behavior
- cleans up test suite
- ensures `deleteJobOnComplete` does not delete definitively failed jobs
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1210553277813320
### What?
This update improves the flexibility of the ConfirmationModal by
allowing the `heading` and `body` prop to accept either a string or a
React node.
If the `heading` or `body` is a string, it will be wrapped in its
respective tags for consistent styling.
If it's already a React element, it will be rendered as-is. This
prevents layout issues when passing JSX content like lists, links, or
formatted elements into the modal heading and body.
Adds eslint rule `no-restricted-exports` with value `off` for payload
config files inside our `test` suite since we have to export with
default from those
🤖 Automated bump of templates for v3.43.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Previously, there were multiple ways to type a running job:
- `GeneratedTypes['payload-jobs']` - only works in an installed project
- is `any` in monorepo
- `BaseJob` - works everywhere, but does not incorporate generated types
which may include type for custom fields added to the jobs collection
- `RunningJob<>` - more accurate version of `BaseJob`, but same problem
This PR deprecated all those types in favor of a new `Job` type.
Benefits:
- Works in both monorepo and installed projects. If no generated types
exist, it will automatically fall back to `BaseJob`
- Comes with an optional generic that can be used to narrow down
`job.input` based on the task / workflow slug. No need to use a separate
type helper like `RunningJob<>`
With this new type, I was able to replace every usage of
`GeneratedTypes['payload-jobs']`, `BaseJob` and `RunningJob<>` with the
simple `Job` type.
Additionally, this PR simplifies some of the logic used to run jobs
Continuation of https://github.com/payloadcms/payload/pull/6245.
This PR allows you to pass `blocksAsJSON: true` to SQL adapters and the
adapter instead of aligning with the SQL preferred relation approach for
blocks will just use a simple JSON column, which can improve performance
with a large amount of blocks.
To try these changes you can install `3.43.0-internal.c5bbc84`.
### What?
Adds `constructorOptions` property to the upload config to allow any of
[these options](https://sharp.pixelplumbing.com/api-constructor/) to be
passed to the Sharp library.
### Why?
Users should be able to extend the Sharp library config as needed, to
define useful properties like `limitInputPixels` etc.
### How?
Creates new config option `constructorOptions` which passes any
compatible options directly to the Sharp library.
#### Reported by client.
Fixes https://github.com/payloadcms/payload/issues/12776
- Adds a new `jobs.config.enabled` property to the sanitized config,
which can be used to easily check if the jobs system is enabled (i.e.,
if the payload-jobs collection was added during sanitization). This is
then checked before Payload sets up job autoruns.
- Fixes some type issues that occurred due to still deep-requiring the
jobs config - we forgot to omit it from the `DeepRequired(Config)` call.
The deep-required jobs config was then incorrectly merged with the
sanitized jobs config, resulting in a SanitizedConfig where all jobs
config properties were marked as required, even though they may be
undefined.
The custom save button showing on export enabled collections in the edit
view. Instead it was meant to only appear in the export collection. This
makes it so it only appears in the export drawer.
Type declaration extending `custom.['plugin-import-export']` was
incorrectly named `toCSVFunction` instead of `toCSV`. This changes the
type to match the correct property name `toCSV`.
By default, calling `payload.jobs.run()` will incorrectly run all jobs
from all queues. The `npx payload jobs:run` bin script behaves the same
way.
The `payload-jobs/run` endpoint runs jobs from the `default` queue,
which is the correct behavior.
This PR does the following:
- Change `payload.jobs.run()` to only runs jobs from the `default` queue
by default
- Change `npx payload jobs:run` bin script to only runs jobs from the
`default` queue by default
- Add new allQueues / --all-queues arguments/queryparams/flags for the
local API, rest API and bin script to allow you to run all jobs from all
queues
- Clarify the docs
Introduces the ability to customize the order of both default and custom
tabs. This way you can make custom tabs appear before default ones, or
change the order of tabs as you see fit.
To do this, use the new `tab.order` property in your edit view's config:
```ts
import type { CollectionConfig } from 'payload'
export const MyCollectionConfig: CollectionConfig = {
// ...
admin: {
components: {
views: {
edit: {
myCustomView: {
path: '/my-custom-view',
Component: '/path/to/component',
tab: {
href: '/my-custom-view',
order: 100, // This will put this tab in the first position
},
}
}
}
}
}
}
```
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Simplifies the rendering logic around document tabs in the following
ways:
- Merges default tabs with custom tabs in a more predictable way, now
there is only a single array of tabs to iterate over, no more concept of
"custom" tabs vs "default" tabs, there's now just "tabs"
- Deduplicates rendering conditions for all tabs, now any changes to
default tabs would also apply to custom tabs with half the code
- Removes unnecessary `getCustomViews` function, this is a relic of the
past
- Removes unnecessary `getViewConfig` function, this is a relic of the
past
- Removes unused `references`, `relationships`, and `version` key
placeholders, these are relics of the past
- Prevents tab conditions from running twice unnecessarily
- Other misc. cleanup like unnecessarily casting the tab conditions
result to a boolean, etc.
### What?
Updates the redirects plugin types to make collections a required type
### Why?
Currently not including the collections object when importing the plugin
causes an error to occur when going to the page in the UI, also it
cannot generate types. Likely due to it unable to make a reference to a
collection.
### How?
Makes collections required
Fixes#12709
---------
Co-authored-by: Patrick Roelofs <patrick.roelofs@iquality.nl>
Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
### What?
Fixes inconsistent `pill` sizes across the Admin Panel.
### How?
Pills without a specified size default to **medium**. In the folders
[PR](https://github.com/payloadcms/payload/pull/10030), additional
padding was to the medium size. As a result, any pills without an
explicit size now appear larger than intended.
This PR fixes that by updating any pills that should be small to
explicitly set `size="small"`.
Fixes#12752
Simplifies document routing in the following ways:
- Removes duplicative code blocks that made it easy to make changes to
collections but not globals
- Consolidates `CustomView`, `DefaultView`, and `ErrorView` into just
`View`
- Removes unnecessary `overrideDocPermissions` arg
- Standardizes the 404 logic when the doc lacks read access
- Fixes styling issue where `UnauthorizedView` is rendered without
margins, e.g. when you lack permission to read versions but navigate to
`/versions`
Filters URLs to avoid issues with SSRF
Had to use `undici` instead of native `fetch` because it was the only
viable alternative that supported both overriding agent/dispatch and
also implemented `credentials: include`.
[More info
here.](https://blog.doyensec.com/2023/03/16/ssrf-remediation-bypass.html)
---------
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
Customizing the `path` property on default document views is currently
not supported, but the types suggest that it is. You can only provide a
path to custom views. This PR ensures that `path` cannot be set on
default views as expected.
For example:
```ts
import type { CollectionConfig } from 'payload'
export const MyCollectionConfig: CollectionConfig = {
// ...
admin: {
components: {
views: {
edit: {
default: {
path: '/' // THIS IS NOT ALLOWED!
},
myCustomView: {
path: '/edit', // THIS IS ALLOWED!
Component: '/collections/CustomViews3/MyEditView.js#MyEditView',
},
},
},
},
},
}
```
For background context, this was deeply explored in #12701. This is not
planned, however, due to [performance and maintainability
concerns](https://github.com/payloadcms/payload/pull/12701#issuecomment-2963926925),
plus [there are alternatives to achieve
this](https://github.com/payloadcms/payload/pull/12772).
This PR also fixes and improves various jsdocs, and fixes a typo found
in the docs.
When saving a global with versioning enabled as draft, and then
publishing it, the following error may appear: `[16:50:35] ERROR: Could
not find createdAt or updatedAt in latestVersion.version`
This is due to an incorrect check to appease typescript strict mode. We
shouldn't throw if `version.updatedAt` doesn't exist - the purpose of
this logic is to add that property if it doesn't exist
Currently, we globally enable both DOM and Node.js types. While this
mostly works, it can cause conflicts - particularly with `fetch`. For
example, TypeScript may incorrectly allow browser-only properties (like
`cache`) and reject valid Node.js ones like `dispatcher`.
This PR disables DOM types for server-only packages like payload,
ensuring Node-specific typings are applied. This caught a few instances
of incorrect fetch usage that were previously masked by overlapping DOM
types.
This is not a perfect solution - packages that contain both server and
client code (like richtext-lexical or next) will still suffer from this
issue. However, it's an improvement in cases where we can cleanly
separate server and client types, like for the `payload` package which
is server-only.
## Use-case
This change enables https://github.com/payloadcms/payload/pull/12622 to
explore using node-native fetch + `dispatcher`, instead of `node-fetch`
+ `agent`.
Currently, it will incorrectly report that `dispatcher` is not a valid
property for node-native fetch
### What?
The azure storage adapter returns a 500 internal server error when a
file is not found.
It's expected that it will return 404 when a file is not found.
### Why?
There is no checking if the blockBlobClient exists before it's used, so
it throws a RestError when used and the blob does not exist.
### How?
Check if exception thrown is of type RestError and have a 404 error from
the Azure API and return a 404 in that case.
An alternative way would be to call the exists() method on the
blockBlobClient, but that will be one more API call for blobs that does
exist. So I chose to check the exception instead.
Also added integration tests for azure storage in the same manner as s3,
as it was missing for azure storage.
### What?
The results when querying orderable collections can be incorrect due to
how the underlying database handles sorting when capitalized letters are
introduced.
### Why?
The original fractional indexing logic uses base 62 characters to
maximize the amount of data per character. This optimization saves a few
characters of text in the database but fails to return accurate results
when mixing uppercase and lowercase characters.
### How?
Instead we can use base 36 values instead (0-9,a-z) so that all
databases handle the sort consistently without needing to introduce
collation or other alternate solutions.
Fixes#12397
### What
This PR updates the `afterChange` hook for collections and globals to
include the `data` argument.
While the `doc` argument provides the saved version of the document,
having access to the original `data` allows for additional context—such
as detecting omitted fields, raw client input, or conditional logic
based on user-supplied data.
### Changes
- Adds the `data` argument to the `afterChange` hook args.
- Applies to both `collection` and `global` hooks.
### Example
```
afterChange: [
({ context, data, doc, operation, previousDoc, req }) => {
if (data?.customFlag) {
// Perform logic based on raw input
}
},
],
```
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
In a similar vein to #11734, #11733, #10327 - this PR returns a 404 in
the response when a file is not found while using the `storage-gcs`
adapter. Currently a 500 is returned.
### Why?
To return the correct error level in the response when a file is not
found when using `storage-gcs`.
### How?
The GCS nodejs library exposes the `ApiError` as a general error - these
changes check that the caught error is an instance of this class and if
the provided code is a `404`.
### What?
This PR removes an extra colon from the `"workspaces"` key which was
likely a typo.
### Why?
To use a properly recognized workspaces key without the extra colon.
### How?
Deletion of `:` from the workspaces key in `package.json`
Previously, every time the drawer re-rendered a new entry animation may
be triggered. This PR fixes this by setting the open state to
`modalState[slug]?.isOpen` instead of `false`.
Additionally, I was able to simplify this component while maintaining
functionality. Got rid of one `useEffect` and one `useState` call. The
remaining useEffect also runs less often (previously, it ran every time
`modalState` changed => it re-ran if _any_ modal opened or closed, not
just the current one)
🤖 Automated bump of templates for v3.42.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
When making an export of a collection, and no fields are selected then
you get am empty CSV. The intended behavior is that all data is exported
by default.
This fixes the issue that from the admin UI, when the fields selector
the resulting CSV has no columns.
Fixes https://github.com/payloadcms/payload/issues/12628
When using sqlite, the error from the db is a bit different than
Postgres.
This PR allows us to extract the fieldName when using sqlite for the
unique constraint error.
### What?
This PR updates the import names for the Bengali (Bangladesh) and
Bengali (India) translations to follow the camelCase convention used for
other hyphenated locales:
- `bnBD → bnBd`
- `bnIN → bnIn`
This aligns with the existing pattern already used for translations like
`zhTw.`
Locale keys in the `translations` map (`'bn-BD'`, `'bn-IN'`) remain
unchanged.
Important: An intentional effort is being made during migration to not
modify runtime behavior. This implies that there will be several
assertions, non-null assertions, and @ts-expect-error. This philosophy
applies only to migrating old code to TypeScript strict, not to writing
new code. For a more detailed justification for this reasoning, see
#11840 (comment).
In this PR, instead of following the approach of migrating a subset of
files, I'm migrating all files by disabling specific rules. The first
commits are named after the rule being disabled.
With this PR, the migration of the payload package is complete 🚀
Adds support for read replicas
https://orm.drizzle.team/docs/read-replicas that can be used to offload
read-heavy traffic.
To use (both `db-postgres` and `db-vercel-postgres` are supported):
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
database: postgresAdapter({
pool: {
connectionString: process.env.POSTGRES_URL,
},
readReplicas: [process.env.POSTGRES_REPLICA_URL],
})
```
---------
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
Previously, if you enabled presigned URL downloads for a collection, all
the files would use them. However, it might be possible that you want to
use presigned URLs only for specific files (like videos), this PR allows
you to pass `shouldUseSignedURL` to control that behavior dynamically.
```ts
s3Storage({
collections: {
media: {
signedDownloads: {
shouldUseSignedURL: ({ collection, filename, req }) => {
return req.headers.get('X-Disable-Signed-URL') !== 'false'
},
},
},
},
})
```
### What?
In a project that has `jobs` configured and the import/export plugin
will error on start:
`payload-jobs validation failed: taskSlug: createCollectionExport is not
a valid enum value for path taskSlug.`
### Why?
The plugin was not properly extending the jobs configuration.
### How?
Properly extend existing config.jobs to add the `createCollectionExport`
task.
Fixes #
This makes it possible to add custom logic into how we map the document
data into the CSV data on a field-by-field basis.
- Allow custom data transformation to be added to
`custom.['plugin-import-export'].toCSV inside the field config
- Add type declaration to FieldCustom to improve types
- Export with `depth: 1`
Example:
```ts
{
name: 'customRelationship',
type: 'relationship',
relationTo: 'users',
custom: {
'plugin-import-export': {
toCSV: ({ value, columnName, row, siblingDoc, doc }) => {
row[`${columnName}_id`] = value.id
row[`${columnName}_email`] = value.email
},
},
},
},
```
Fixes https://github.com/payloadcms/payload/issues/12639
Currently, if an `importmap.js` file is not found — it throws an error.
This change allows the file to be created if the directory can be found.
If you specify your own `importmap` location in the config, it will
attempt to create when missing and throw an error if it cannot.
Fixes#12723
## Problem
With two or more state groups, a falsy state entry would run
`dom.style.cssText = ''`, wiping all inline styles - including valid
styles just set by earlier groups. This caused earlier groups to lack
css styling.
## Solution
**1. Collect first, apply once**
During the stateMap.forEach loop, gather each active group's styles into
a shared `mergedStyles` object.
Inactive groups still clear their own `data-*` attribute but leave
styles untouched.
**2. Single reset + single write**
After the loop, perform one cssText reset, instead of one cssText in
each iteration. Then apply each state group's css all at once, after the
blank reset
Result: every state group can coexist without overwriting styles from
the others
### What?
Add a warning to the form builder plugin docs about potential GraphQL
type name collisions with custom Blocks or Collections.
### Why?
To help users avoid schema errors caused by conflicting type names and
guide them with resolution options.
### What?
Adds documentation to demonstrate how to make the internal
`payload-jobs` collection visible in the Admin Panel using the
`jobsCollectionOverrides` option.
### Why?
By default, the jobs collection is hidden from the UI. However, for
debugging or monitoring purposes—especially during development—some
teams may want direct access to view and inspect job entries.
### How?
A code snippet is added under the **"Jobs Queue > Overview"** page
showing how to override the collection config to set `admin.hidden =
false`.
---
**Before:**
No mention of how to expose the `payload-jobs` collection in the
documentation.
**After:**
Clear section and code example on how to enable visibility for the jobs
collection.
---
Supersedes: [#12321](https://github.com/payloadcms/payload/pull/12321)
Related Discord thread: [Payload Jobs
UI](https://discord.com/channels/967097582721572934/1367918548428652635)
Co-authored-by: Willian Souza <willian.souza@compasso.com.br>
The i18n namespace `movingFromFolder`'s second dynamic variable name
should be `{{fromFolder}}`, but lots of locales use `{{folderName}}`, so
it will fail to get the value.
8199a7d32a/packages/ui/src/elements/FolderView/Drawers/MoveToFolder/index.tsx (L360-L363)
There is also some optimization for Folder related translation and
generic terms of zh.ts included inside.
## Description
Bangla translations for the admin panel
- [x] I have read and understand the CONTRIBUTING.md document in this
repository
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## Checklist:
- [ ] Existing test suite passes locally with my changes
No issues with turbopack reported so far, let's enable it by default in
our monorepo. The `--turbo` flag for our package.json `dev` and
`test:e2e` scripts has been replaced with an opt-out `--no-turbo` flag
This clarifies that jobs.autoRun only *runs* already-queued jobs. It does not queue the jobs for you.
Also adds an e2e test as this functionality had no e2e coverage
TextStateFeature wasn't intended to be used without props, but it still
shouldn't throw a runtime error if used that way. Perhaps some users are
experimenting until they decide on the props.
Fixes#12518
The module `@payloadcms/plugin-nested-docs/fields` does not seem to
exist (anymore). Instead `createParentField` and
`createBreadcrumbsField` are exported by
`@payloadcms/plugin-nested-docs`
There are various open issues relating to the beforeChange hook as well
as statements from payload team about its behaviour that conflict with
the docs - this brings the docs in line with the expected behaviour of
the hook
Current expected behaviour:
https://github.com/payloadcms/payload/issues/9714#issuecomment-2710872473
beforeChange open issues:
https://github.com/payloadcms/payload/issues/12065https://github.com/payloadcms/payload/issues/11169https://github.com/payloadcms/payload/issues/9714
We should probably acknowledge, as part of this documentation change for
discussion, that while this update reflects the current behavior, it
raises questions about the efficacy of the hook and whether this is
truly the desired behavior.
I suspect users want the behaviour as documented today, not the modified
version, but have not realised the true implementation detail through
error or external abuse yet. It is hard to detect problems that arise
from this when using the admin UI as it obscures them with the
validation errors while not making it obvious that the hook still ran.
I would suggest that having the data passed into this hook as strongly
typed instead of Partial<collection> does not aid developers in
understanding how this hook works.
The short version: **I think there is a requirement for a hook that runs
before the database write but with valid data, and i think people
believe this is that hook.**
The original documentation was unnecessarily complex - you don't need to
import the original interface and extend from it in order to add
additional properties to it via module augmentation.
Follow up to #12404.
Templates include a custom route for demonstration purposes that shows
how to get Payload and use it. It was intended that these routes are
either removed or modified for every new project, however, we can't
guarantee this. This means that they should not expose any sensitive
data, such as the users list.
Instead, we can return a simple message from these routes indicating
they are custom. This will ensure that even if they are kept as-is and
deployed, no sensitive data is leaked. Payload is still instantiated,
but we simply don't use it.
This PR also types the first argument to further help users get started
building custom routes.
This PR re-uses the document drawers for editing and creating folders.
This allows us to easily render document fields that are added inside
`collectionOverrides` on the folder config.
Not much changed, the folder drawer UI now resembles what you would
expect when you create a document in payload. It is a bit slimmed back
but generally very similar.
I noticed a few issues when running e2e tests that will be resolved by
this PR:
- Most important: for some test suites (fields, fields-relationship,
versions, queues, lexical), the database was cleared and seeded
**twice** in between each test run. This is because the onInit function
was running the clear and seed script, when it should only have been
running the seed script. Clearing the database / the snapshot workflow
is being done by the reInit endpoint, which then calls onInit to seed
the actual data.
- The slowest part of `clearAndSeedEverything` is recreating indexes on
mongodb. This PR slightly improves performance here by:
- Skipping this process for the built-in `['payload-migrations',
'payload-preferences', 'payload-locked-documents']` collections
- Previously we were calling both `createIndexes` and `ensureIndexes`.
This was unnecessary - `ensureIndexes` is a deprecated alias of
`createIndexes`. This PR changes it to only call `createIndexes`
- Makes the reinit endpoint accept GET requests instead of POST requests
- this makes it easier to debug right in the browser
- Some typescript fixes
- Adds a `dev:memorydb` script to the package.json. For some reason,
`dev` is super unreliable on mongodb locally when running e2e tests - it
frequently fails during index creation. Using the memorydb fixes this
issue, with the bonus of more closely resembling the CI environment
- Previously, you were unable to run test suites using turbopack +
postgres. This fixes it, by explicitly installing `pg` as devDependency
in our monorepo
- Fixes jest open handles warning
Adds configurations for browse-by-folder document results. This PR
**does NOT** allow for filtering out folders on a per collection basis.
That will be addressed in a future PR 👍
### Disable browse-by-folder all together
```ts
type RootFoldersConfiguration = {
/**
* If true, the browse by folder view will be enabled
*
* @default true
*/
browseByFolder?: boolean
// ...rest of type
}
```
### Remove document types from appearing in the browse by folder view
```ts
type CollectionFoldersConfiguration =
| boolean
| {
/**
* If true, the collection documents will be included in the browse by folder view
*
* @default true
*/
browseByFolder?: boolean
}
```
### Misc
Fixes https://github.com/payloadcms/payload/issues/12631 where adding
folders.collectionOverrides was being set on the client config - it
should be omitted.
Fixes an issue where `baseListFilters` were not being respected.
### What?
This PR fixes an issue while using `text` & `number` fields with
`hasMany: true` where the last entry would be unreachable, and thus
undeletable, because the `transformForWrite` function did not track
these rows for deletion. This causes values that should've been deleted
to remain in the edit view form, as well as the db, after a submission.
This PR also properly threads the placeholder value from
`admin.placeholder` to `text` & `number` `hasMany: true` fields.
### Why?
To remove rows from the db when a submission is made where these fields
are empty arrays, and to properly show an appropriate placeholder when
one is set in config.
### How?
Adjusting `transformForWrite` and the `traverseFields` to keep track of
rows for deletion.
Fixes#11781
Before:
[Editing---Post-dbpg-before--Payload.webm](https://github.com/user-attachments/assets/5ba1708a-2672-4b36-ac68-05212f3aa6cb)
After:
[Editing---Post--dbpg-hasmany-after-Payload.webm](https://github.com/user-attachments/assets/1292e998-83ff-49d0-aa86-6199be319937)
Previously, this was possible in MongoDB but not in Postgres/SQLite
(having `null` in an `in` query)
```
const { docs } = await payload.find({
collection: 'posts',
where: { text: { in: ['text-1', 'text-3', null] } },
})
```
This PR fixes that behavior
In one of the versions we've changed the type of the argument from
`I18n<any, any>` to `I18n<unknown, unknown>` and this has caused some
issues with TS resolving the type compatibility in the `formatDate`
utility so it no longer supports `I18nClient`.
This type change happened in
https://github.com/payloadcms/payload/pull/10030
- The `ConfigProvider` was unnecessarily sanitizing the client config
twice on initial render, leading to an unnecessary re-render. Now it
only happens once
- Memoizes the context value to prevent accidental, unnecessary
re-renders of consumers
We've already supported `autoComplete` on admin config for text fields
but it wasn't being threaded through to the text input element so it
couldn't be applied.
Closes https://github.com/payloadcms/payload/issues/12355
TabbedUI doesn't always work as intended and it can be affected by
existing fields or the order of other plugins, this mentions that and
links to the recommended direct use of fields example.
Important: An intentional effort is being made during migration to not
modify runtime behavior. This implies that there will be several
assertions, non-null assertions, and @ts-expect-error. This philosophy
applies only to migrating old code to TypeScript strict, not to writing
new code. For a more detailed justification for this reasoning,
https://github.com/payloadcms/payload/pull/11840#discussion_r2021975897.
In this PR, instead of following the approach of migrating a subset of
files, I'm migrating all files by disabling a specific rule. In this
case, `strictNullChecks`.
`strictNullChecks` is a good rule to start the migration with because
it's easy to silence with non-null assertions or optional chainings.
Additionally, almost all ts strict errors are due to this rule.
This PR improves 200+ files, leaving only 68 remaining to migrate to
strict mode in the payload package.
## What
Adds a new custom component called `editMenuItems` that can be used in
the document view.
This options allows users to inject their own custom components into the
dropdown menu found in the document controls (the 3 dot menu), the
provided component(s) will be added below the default existing actions
(Create New, Duplicate, Delete and so on).
## Why
To increase flexibility and customization for users who wish to add
functionality to this menu. This provides a clean and consistent way to
add additional actions without needing to override or duplicate existing
UI logic.
## How
- Introduced the `editMenuItems` slot in the document controls dropdown
(three-dot menu) - in edit and preview tabs.
- Added documentation and tests to cover this new custom component
#### Testing
Use the `admin` test suite and go to the `edit menu items` collection
Fixes https://github.com/payloadcms/payload/issues/12532
Normally we clear any values when picking a date such that your hour,
minutes and seconds are normalised to 0 unless specified. Equally when
you specify a time we will normalise seconds so that only minutes are
relevant as configured.
Miliseconds were never removed from the actual date value and whatever
milisecond the editor was in was that value that was being added.
There's this [abandoned
issue](https://github.com/Hacker0x01/react-datepicker/issues/1991) from
the UI library `react-datepicker` as it's not something configurable.
This fixes that problem by making sure that miliseconds are always 0
unless the `displayFormat` includes `SSS` as an intention to show and
customise them.
This also caused [issues with scheduled
jobs](https://github.com/payloadcms/payload/issues/12566) if things were
slightly out of order or not being scheduled in the expected time
interval.
This adds a new `tests-e2e-turbo` CI step that runs our e2e test suite
against turbo. This will ensure that we can guarantee full support for
turbopack.
Our CI runners are already at capacity, so the turbo steps will only run
if the `tests-e2e-turbo` label is set on the PR.
This PR introduces a few changes to improve turbopack compatibility and
ensure e2e tests pass with turbopack enabled
## Changes to improve turbopack compatibility
- Use correct sideEffects configuration to fix scss issues
- Import scss directly instead of duplicating our scss rules
- Fix some scss rules that are not supported by turbopack
- Bump Next.js and all other dependencies used to build payload
## Changes to get tests to pass
For an unknown reason, flaky tests flake a lot more often in turbopack.
This PR does the following to get them to pass:
- add more `wait`s
- fix actual flakes by ensuring previous operations are properly awaited
## Blocking turbopack bugs
- [X] https://github.com/vercel/next.js/issues/76464
- Fix PR: https://github.com/vercel/next.js/pull/76545
- Once fixed: change `"sideEffectsDisabled":` back to `"sideEffects":`
## Non-blocking turbopack bugs
- [ ] https://github.com/vercel/next.js/issues/76956
## Related PRs
https://github.com/payloadcms/payload/pull/12653https://github.com/payloadcms/payload/pull/12652
This PR makes it possible to do polymorphic join querying by fields that
don't exist in all collections specified in `field.collection`, for
example:
```
const result = await payload.find({
collection: 'payload-folders',
joins: {
documentsAndFolders: {
where: {
and: [
{
relationTo: {
in: ['folderPoly1', 'folderPoly2'],
},
},
{
folderPoly2Title: { // this field exists only in the folderPoly2 collection, before it'd throw a query error.
equals: 'Poly 2 Title',
},
},
],
},
},
},
})
```
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
This PR uses the new `useQueue` hook for relationship react-select field
for loading options. This will reduce flakiness in our CI and ensure the
following:
- most recently triggered options loading request will not have its
result overwritten by a previous, delayed request
- reduce unnecessary, parallel requests - outdated requests are
discarded from the queue if a newer request exist
### What?
TypeScript says that it is possible to modify the tab of the default
view however, when you specify the path to the custom component, nothing
happens. I fixed it.
### How?
If a Component for the tab of the default view is defined in the config,
I return that Component instead of DocumentTab
### Example Configuration
config.ts
```ts
export const MenuGlobal: GlobalConfig = {
slug: menuSlug,
fields: [
{
name: 'globalText',
type: 'text',
},
],
admin: {
components: {
views: {
edit: {
api: {
tab: {
Component: './TestComponent.tsx',
},
},
},
},
},
},
}
```
./TestComponent.tsx
```tsx
const TestComponent = () => 'example'
export default TestComponent
```
### Before

### After

---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
### What?
The browser was incorrectly setting the mimetype for `.glb` and `.gltf`
files to `application/octet-stream` when uploading when they should be
receiving proper types consistent with `glb` and `gltf`.
This patch adds logic to infer the correct `MIME` type for `.glb` files
(`model/gltf-binary`) & `gltf` files (`model/gltf+json`) based on file
extension during multipart processing, ensuring consistent MIME type
detection regardless of browser behavior.
Fixes#12620
### What?
Fixes `resetColumnsState` in `useTableColumns` react hook.
### Why?
`resetColumnsState` threw errors when being executed, e.g. `Uncaught (in
promise) TypeError: Cannot read properties of undefined (reading
'findIndex')`
### How?
Removes unnecessary parsing of URL query parameters in
`setActiveColumns` when resetting columns.
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
This PR fixes an issue with `plugin-seo` where the `MetaImageComponent`
would not allow creating a new upload document from the field.
### Why?
To allow users to upload new media documents for use as a meta image.
### How?
Threads `allowCreate` through to the underlying upload input.
Fixes#12616
Before:

After:

Fixes#6871
To review this PR, use `pnpm dev lexical` and the auto-created document
in the `lexical fields` collection. Select any input within the blocks
and press `cmd+a`. The selection should contain the entire input.
I made sure that `cmd+a` still works fine inside the editor but outside
of inputs.
When cloning a new project from the examples dir via create-payload-app,
the corresponding `.env` file is not being generated. This is because
the `--example` flag does not prompt for database credentials, which
ultimately skips this step.
For example:
```bash
npx create-payload-app --example live-preview
```
The result will include the provided `.env.example`, but lacks a `.env`.
We were previously writing to the `.env.example` file, which is
unexpected. We should only be writing to the `.env` file itself. To do
this, we only write the `.env.example` to memory as needed, instead of
the file system.
This PR also simplifies the logic needed to set default vars, and
improves the testing coverage overall.
Type inferences broke as a result of migrating to ts strict mode in
#12298. This leads to compile-time errors that may prevent build.
Here is an example:
```ts
export interface Page {
id: string;
slug: string;
title: string;
// ...
}
/**
* Type 'Page' does not satisfy the constraint 'Record<string, unknown>'.
* Index signature for type 'string' is missing in type 'Page'.
*/
const { data } = useLivePreview<Page>({
depth: 2,
initialData: initialPage,
serverURL: PAYLOAD_SERVER_URL,
})
```
The problem is that Payload generated type _interfaces_ do not satisfy
the `Record<string, unknown>` type. This is because interfaces are a
possible target for declaration merging, so their properties are not
fully known. More details on this
[here](https://github.com/microsoft/TypeScript/issues/42825).
This PR also cleans up the JSDocs.
Technically you could already set `label: undefined` and it would be
supported by group fields but the types didn't reflect this.
So now you can create an unnamed group field like this:
```ts
{
type: 'group',
fields: [
{
type: 'text',
name: 'insideGroupWithNoLabel',
},
],
},
```
This will remove the label while still visually grouping the fields.

---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
🤖 Automated bump of templates for v3.40.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- I corrected the use of `yarn` instead of `pnpm`
- I corrected the URL for previewing the documentation (it was missing
`/local`).
- I removed the incorrect section about what type of commit falls into
the release notes.
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Exposes a new argument to authentication strategies which allows the
author to determine if this auth strategy has the capability of setting
response headers or not.
This is useful because some auth strategies may want to set headers, but
in Next.js server components (AKA the admin panel), it's not possible to
set headers. It is, however, possible to set headers within API
responses and similar contexts.
So, an author might decide to only run operations that require setting
headers (i.e. refreshing an access token) if the auth strategy is being
executed in contexts where setting headers is possible.
Following up on https://github.com/payloadcms/payload/pull/12515, we
could instead use the same `parseCookies` method that Next.js uses. This
handles a few edge-cases differently:
- correctly strips whitespace
- parses attributes without explicit values
I think it's a good idea to match the behavior of Next.js as close as
possible here. [This](https://github.com/vercel/edge-runtime/pull/374)
is a good example of how the Next.js behavior behaves differently.
## Example
Input: `'my_value=true; Secure; HttpOnly'`
Previous Output:
```
Map(3) {
'my_value' => 'true',
'Secure' => '',
'HttpOnly' => '',
}
```
New Output:
```
Map(3) {
'my_value' => 'true',
'Secure' => 'true',
'HttpOnly' => 'true'
}
```
this has been already reported here:
https://github.com/payloadcms/payload/issues/10591
`parseCookies.ts` tries to decode cookie's values using `decodeURI()`
and throws an Error when it fails
Since it does that on all cookies set on the current domain, there's no
control on which cookie get evaluated; for instance ads networks,
analytics providers, external fonts, etc... all set cookies with
different encodings.
### Taking in consideration:
- HTTP specs doesn't define a standard way for cookie value encoding but
simply provide recommendations:
[RFC6265](https://httpwg.org/specs/rfc6265.html#sane-set-cookie)
> To maximize compatibility with user agents, servers that wish to store
arbitrary data in a cookie-value SHOULD encode that data, for example,
using Base64
- NextJS does a pretty similar parsing and ignore invalid encoded values
https://github.com/vercel/edge-runtime/blob/main/packages/cookies/src/serialize.ts
`function parseCookie(cookie: string)`
```typescript
try {
map.set(key, decodeURIComponent(value ?? 'true'))
} catch {
// ignore invalid encoded values
}
```
### With the current implementation:
- it's impossible to login because `parseCookies.ts` get called and if
fails to parse throws and error
- requests to `/api/users/me` fail for the same reason
### Fix
the pull request address these issues by simply ignoring decoding
errors:
CURRENT:
```typescript
try {
const decodedValue = decodeURI(encodedValue)
list.set(key, decodedValue)
} catch (e) {
throw new APIError(`Error decoding cookie value for key ${key}: ${e.message}`)
}
```
AFTER THIS PULL REQUEST
```typescript
try {
const decodedValue = decodeURI(encodedValue)
list.set(key, decodedValue)
} catch {
// ignore invalid encoded values
}
```
Fixes#12588
Previously, the new `disableBlockName` was not respected for lexical
blocks. This PR adds a new e2e test and does some clean-up of previous
e2e tests
There are still things to improve.
- We're inconsistent with our use of capital letters. There are still
sentences where every word starts with a capital letter, and it looks
ugly (this also happens in English, but to a lesser extent).
- We're inconsistent with the use of punctuation at the end.
- Sentences with variables like {{count}} can result in inconsistencies
if it's 1 and the noun is plural.
- The same thing happens in Spanish, but also with gender. It's
impossible to know without the context in which it's used.
---------
Co-authored-by: Paul Popus <paul@payloadcms.com>
To reproduce this bug, insert the following feature into the richtext
editor:
```ts
BlocksFeature({
inlineBlocks: [
{
slug: 'inline-media',
fields: [
{
name: 'media',
type: 'relationship',
relationTo: ['media'],
admin: {
appearance: 'drawer',
},
},
],
},
],
}),
```
Then try opening the relationship field drawer. The inline block drawer
will close.
Note: Interestingly, at least in Chrome, this only happens with DevTools
closed. It worked fine with DevTools open. It probably has to do with
capturing events like focus.
The current solution is a 50ms delay. I couldn't test it with CPU
throttle because it disappears when I close the devtools. If you
encounter this bug, please open an issue so we can increase the delay
or, better yet, find a more elegant solution.
- update SantizeCollection to SanitizedCollection in TypeScript section
- fix new issue link in Multi-Tenant plugin section
- correct You can disabled to disable in Core Features
- fix duplicate section links for MongoDB and Postgres in Migrations
section
### What?
1. Adds logic to automatically update the `importMap.js` file with the
project name provided by the user.
2. Adds an updated version of the `README.md` file that we had when this
template existed outside of the monorepo
([here](https://github.com/payloadcms/plugin-template/blob/main/README.md))
to provide clear instructions of required steps.
### Why?
1. The plugin template when installed via `npx create-payload-app` asks
the user for a project name, however the exports from `importMap.js` do
not get updated to the provided name. This throws errors when running
the project and prevents it from building.
2. The `/dev` folder requires the `.env.example` to be copied and
renamed to `.env` - the project will not run until this is done. The
template lacks instructions that this is a required step.
### How?
1. Updates
`packages/create-payload-app/src/lib/configure-plugin-project.ts` to
read the `importMap.js` file and replace the placeholder plugin name
with the name provided by the users. Adds a test to
`packages/create-payload-app/src/lib/create-project.spec.ts` to verify
that this file gets updated correctly.
2. Adds instructions on using this template to the `README.md` file,
ensuring key steps (like adding the `.env` file) are clearly stated.
Additional housekeeping updates:
- Removed Jest and replaced it with Vitest for testing
- Updated the base test approach to use Vitest instead of Jest
- Removed `NextRESTClient` in favor of directly creating Request objects
- Abstracted `getCustomEndpointHandler` function
- Added ensureIndexes: true to the mongooseAdapter configuration
- Removed the custom server from the dev folder
- Updated the pnpm dev script to "dev": "next dev dev --turbo"
- Removed `admin.autoLogin`
Fixes#12198
### What
Continuation of #7355 by extending the functionality to named tabs.
Updates `flattenFields` to hoist nested fields inside named tabs to the
top-level field array when `moveSubFieldsToTop` is enabled.
Also fixes an issue where group fields with custom cells were being
flattened out.
Now, group fields with a custom cell components remain available as
top-level columns.
Fixes#12563
You can now specify exactly who can change the constraints within a
query preset.
For example, you want to ensure that only "admins" are allowed to set a
preset to "everyone".
To do this, you can use the new `queryPresets.filterConstraints`
property. When a user lacks the permission to change a constraint, the
option will either be hidden from them or disabled if it is already set.
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
queryPresets: {
// ...
filterConstraints: ({ req, options }) =>
!req.user?.roles?.includes('admin')
? options.filter(
(option) =>
(typeof option === 'string' ? option : option.value) !==
'everyone',
)
: options,
},
})
```
The `filterConstraints` functions takes the same arguments as
`reduceOptions` property on select fields introduced in #12487.
The translations would sometimes be using the wrong meanings in other
languages, for example Locale becoming "location" or "region" depending
on the phrase or translation.
Some words were also not being used consistently across the UI which
could cause some confusion if they're interchanged.
I've fixed these instances for locale specifically in dutch and spanish.
I've also updated the prompt with more context around what's being
translated and some examples, over time we should add to the examples so
that translations are better guarded against changing meanings.
---------
Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
The translation engine previously rendered the English term locale as
“lokalitet” or “steder” in Norwegian, which in practice refers to a
geographic location rather than a language variant. This has been
changed throughout. I have also made some other small improvements to
the Norwegian translations.
### What?
Fixes an infinite loop that may occur when invoking the findUp() utility
to search for a file by name and no result is found.
### Why?
If triggered, the infinite loop will hang the entire application. For
example, this occurs when trying to boot Payload from a Cloudflare
Worker, as reported in #12327
### How?
By checking whether it has reached the root directory before analysing
the parent folder
It is a common pattern to dynamically show and validate a select field's
options based on various criteria such as the current user or underlying
document.
Some examples of this might include:
- Restricting options based on a user's role, e.g. admin-only options
- Displaying different options based on the value of another field, e.g.
a city/state selector
While this is already possible to do with a custom `validate` function,
the user can still view and select the forbidden option...unless you
_also_ wired up a custom component.
Now, you can define `filterOptions` on select fields.
This behaves similarly to the existing `filterOptions` property on
relationship and upload fields, except the return value of this function
is simply an array of options, not a query constraint. The result of
this function will determine what is shown to the user and what is
validated on the server.
Here's an example:
```ts
{
name: 'select',
type: 'select',
options: [
{
label: 'One',
value: 'one',
},
{
label: 'Two',
value: 'two',
},
{
label: 'Three',
value: 'three',
},
],
filterOptions: ({ options, data }) =>
data.disallowOption1
? options.filter(
(option) => (typeof option === 'string' ? options : option.value) !== 'one',
)
: options,
}
```
The return value of the `adminsAndUser` method was not a proper Query to
limit the read scope of the read access. So users could read all user
data of the system.
Alongside I streamlined the type imports (fixes#12323) and fixed some
typescript typings. And aligned the export of the mentioned to align
with the other access methods.
🤖 Automated bump of templates for v3.39.1
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
🤖 Automated bump of templates for v3.39.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
CPA would previously install an outdated version of the templates based
on the git tag, this is now set to the `main` branch ensuring that the
latest version is always installed.
### What?
The integration tests in live-preview has been using the
`fieldSchemaToJSON` method with wrong params/types.
It's defined as
```
export const fieldSchemaToJSON = (fields: ClientField[], config: ClientConfig): FieldSchemaJSON
```
In the test setup
`fields` was set to `Pages.fields` which was `Field[]`, not the expected
`ClientField[]`
`config` was set to `config` which was `Promise<SanitizedConfig>` not
the expected `ClientConfig`
### Why?
I'm working on some other changes to live-preview where I need the
proper values wired up correctly to properly add integration tests.
The test has worked up until now because Field is very similar to
ClientField. But it should test with the correct type.
### How?
By creating the clientConfig and using the correct types/params when
calling fieldSchemaToJSON in the test setup.
**Note:** Removed test "Collections - Live Preview › merges data", the
test worked before because **id** field is not part of Field, but part
of ClientField. So test code does not behave like this in real scenario
when real ClientField is used. There are lots of real tests for correct
data, removed this one which seems very simple and not correct.
Originally this PR was going to introduce a `TextColorFeature`, but it
ended up becoming a more general-purpose `TextStateFeature`.
## Example of use:
```ts
import { defaultColors, TextStateFeature } from '@payloadcms/richtext-lexical'
TextStateFeature({
// prettier-ignore
state: {
color: {
...defaultColors,
// fancy gradients!
galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } },
sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } },
},
// You can have both colored and underlined text at the same time.
// If you don't want that, you should group them within the same key.
// (just like I did with defaultColors and my fancy gradients)
underline: {
'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } },
// You'll probably want to use the CSS light-dark() utility.
'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } },
},
},
}),
```
Which will result in the following:

## Challenges & Considerations
Adding colors or styles in general to the Lexical editor is not as
simple as it seems.
1. **Extending TextNode isn't ideal**
- While possible, it's verbose, error-prone, and not composable. If
multiple features extend the same node, conflicts arise.
- That’s why we collaborated with the Lexical team to introduce [the new
State API](https://lexical.dev/docs/concepts/node-replacement)
([PR](https://github.com/facebook/lexical/pull/7117)).
2. **Issues with patchStyles**
- Some community plugins use `patchStyles`, but storing CSS in the
editor’s JSON has drawbacks:
- Style adaptability: Users may want different styles per scenario
(dark/light mode, mobile/web, etc.).
- Migration challenges: Hardcoded colors (e.g., #FF0000) make updates
difficult. Using tokens (e.g., "red") allows flexibility.
- Larger JSON footprint increases DB size.
3. **Managing overlapping styles**
- Some users may want both text and background colors on the same node,
while others may prefer mutual exclusivity.
- This approach allows either:
- Using a single "color" state (e.g., "bg-red" + "text-red").
- Defining separate "bg-color" and "text-color" states for independent
styling.
4. **Good light and dark modes by default**
- Many major editors (Google Docs, OneNote, Word) treat dark mode as an
afterthought, leading to poor UX.
- We provide a well-balanced default palette that looks great in both
themes, serving as a strong foundation for customization.
5. **Feature name. Why TextState?**
- Other names considered were `TextFormatFeature` and
`TextStylesFeature`. The term `format` in Lexical and Payload is already
used to refer to something else (italic, bold, etc.). The term `style`
could be misleading since it is never attached to the editorState.
- State seems appropriate because:
- Lexical's new state API is used under the hood.
- Perhaps in the future we'll want to make state features for other
nodes, such as `ElementStateFeature` or `RootStateFeature`.
Note: There's a bug in Lexical's `forEachSelectedTextNode`. When the
selection includes a textNode partially on the left, all state for that
node is removed instead of splitting it along the selection edge.
Fixes#8168, #8277
The fact that `lexicalHTMLField` doesn't work with live preview was
already clarified at the beginning of the page. I mentioned it again in
the dedicated section because it seems there was still confusion.
Also, I reordered and hierarchized the headings correctly. The
introduction said there were two ways to convert to HTML, but there were
four headings with the same level. I also made the headings a little
shorter to make the table of contents easier to parse.
Adds a new date field to take submission values for.
It can help form serialisers render the right input for this kind of
field as the submissions themselves don't do any validation right now.
Disabled by default as to not cause any conflicts with existing projects
potentially inserting their own date blocks.
Can be enabled like this
```ts
formBuilderPlugin({
fields: {
date: true
}
})
```
Same as https://github.com/payloadcms/payload/pull/10398 but for inline
blocks.
> Reproduction steps:
> 1. Set `strict: true` in `templates/website/tsconfig.json`
> 2. You will find a ts error in
`templates/website/src/components/RichText/index.tsx`.
>
> This is because the blockType property of blocks is generated by
Payload as a literal (e.g. "mediaBlock") and cannot be assigned to a
string.
>
> To test this PR, you can make the change to `JSXConvertersFunction` in
node_modules of the website template
In #12322 we prevented against accidental query preset lockout by
throwing a validation error when the user is going to change the preset
in a way that removes their own access to it. This, however, puts the
responsibility on the user to make the corrections and is an unnecessary
step.
For example, the API currently forbids leaving yourself out of the
`users` array when specifying the `specificUsers` constraint, but when
you encounter this error, have to update the field manually and try
again.
To improve the experience, we now automatically inject the requesting
user onto the `users` array when this constraint is selected. This will
guarantee they have access and prevent an accidental lockout while also
avoiding the API error feedback loop.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
-->
### What?
This PR help to fix an issue you'll encounter while running payload in
OpenNext on cloudflare
### Why?
Sending telemetry event will create an infinite loop because it won't be
able to find a `package.json`
### How?
Putting the whole logic of `sendEvent` behind `config.telemetry` allows
to disable it and thus, make it work on cloudflare
See this comment for more info :
https://github.com/opennextjs/opennextjs-cloudflare/issues/263#issuecomment-2851747956
This PR updates `generateFileData` to skip applying `resizeOptions`
after updating an image if `resizeOptions.withoutEnlargement` is `true`
and the original image size is smaller than the dimensions defined in
`resizeOptions`.
This prevents unintended re-resizing of already resized images when
updating or modifying metadata without uploading a new file.
This change ensures that:
- Resizing is skipped if withoutEnlargement: true
- Resizing still occurs if withoutEnlargement: false or unset
This resolves an issue where images were being resized again
unnecessarily when updating an upload.
Fixes#12280
Converts all text and field labels into variables that can be
translated. Also generated the translations for them
So now the UI here is internationalised

I've also moved some of the generic labels into the core package since
those could be re-used elsewhere
The databases do not keep track of document order internally so when
sorting by non-unique fields such as shared `order` number values, the
returned order will be random and not consistent.
While this issue is far more noticeable on mongo it could also occur in
postgres on certain environments.
This combined with pagination can lead to the perception of duplicated
or inconsistent data.
This PR adds a second sort parameter to queries so that we always have a
fallback, `-createdAt` will be used by default or `id` if timestamps are
disabled.
I think it's easier to review this PR commit by commit, so I'll explain
it this way:
## Commits
1. [parallelize eslint script (still showing logs results in
serial)](c9ac49c12d):
Previously, `--concurrency 1` was added to the script to make the logs
more readable. However, turborepo has an option specifically for these
use cases: `--log-order=grouped` runs the tasks in parallel but outputs
them serially. As a result, the lint script is now significantly faster.
2. [run pnpm
lint:fix](9c128c276a)
The auto-fix was run, which resolved some eslint errors that were
slipped in due to the use of `no-verify`. Most of these were
`perfectionist` fixes (property ordering) and the removal of unnecessary
assertions. Starting with this PR, this won't happen again in the
future, as we'll be verifying the linter in every PR across the entire
codebase (see commit 7).
3. [fix eslint non-autofixable
errors](700f412a33)
All manual errors have been resolved except for the configuration errors
addressed in commit 5. Most were React compiler violations, which have
been disabled and commented out "TODO" for now. There's also an unused
`use no memo` and a couple of `require` errors.
4. [move react-compiler linter to eslint-config
package](4f7cb4d63a)
To simplify the eslint configuration. My concern was that there would be
a performance regression when used in non-react related packages, but
none was experienced. This is probably because it only runs on .tsx
files.
5. [remove redundant eslint config files and fix
allowDefaultProject](a94347995a)
The main feature introduced by `typescript-eslint` v8 was
`projectService`, which automatically searches each file for the closest
`tsconfig`, greatly simplifying configuration in monorepos
([source](https://typescript-eslint.io/blog/announcing-typescript-eslint-v8#project-service)).
Once I moved `projectService` to `packages/eslint-config`, all the other
configuration files could be easily removed.
I confirmed that pnpm lint still works on individual packages.
The other important change was that the pending eslint errors from
commits 2 and 3 were resolved. That is, some files were giving the
error: "[File] was not found by the project service. Consider either
including it in the tsconfig.json or including it in
allowDefaultProject." Below I copy the explanatory comment I left in the
code:
```ts
// This is necessary because `tsconfig.base.json` defines `"rootDir": "${configDir}/src"`,
// And the following files aren't in src because they aren't transpiled.
// This is typescript-eslint's way of adding files that aren't included in tsconfig.
// See: https://typescript-eslint.io/troubleshooting/typed-linting/#i-get-errors-telling-me--was-not-found-by-the-project-service-consider-either-including-it-in-the-tsconfigjson-or-including-it-in-allowdefaultproject
// The best practice is to have a tsconfig.json that covers ALL files and is used for
// typechecking (with noEmit), and a `tsconfig.build.json` that is used for the build
// (or alternatively, swc, tsup or tsdown). That's what we should ideally do, in which case
// this hardcoded list wouldn't be necessary. Note that these files don't currently go
// through ts, only through eslint.
```
6. [Differentiate errors from warnings in VScode ESLint
Rules](5914d2f48d)
There's no reason to do that. If an eslint rule isn't an error, it
should be disabled or converted to a warning.
7. [Disable skip lint, and lint over the entire repo now that it's
faster](e4b28f1360)
The GitHub action linted only the files that had changed in the PR.
While this seems like a good idea, once exceptions were introduced with
[skip lint], they opened the door to propagating more and more errors.
Often, the linter was skipped, not because someone introduced new
errors, but because they were trying to avoid those that had already
crept in, sometimes accidentally introducing new ones.
On the other hand, `pnpm lint` now runs in parallel (commit 1), so it's
not that slow. Additionally, it runs in parallel with other GitHub
actions like e2e tests, which take much longer, so it can't represent a
bottleneck in CI.
8. [fix lint in next
package](4506595f91)
Small fix missing from commit 5
9. [Merge remote-tracking branch 'origin/main' into
fix-eslint](563d4909c1)
10. [add again eslint.config.js in payload
package](78f6ffcae7)
The comment in the code explains it. Basically, after the merge from
main, the payload package runs out of memory when linting, probably
because it grew in recent PRs. That package will sooner or later
collapse for our tooling, so we may have to split it. It's already too
big.
## Future Actions
- Resolve React compiler violations, as mentioned in commit 3.
- Decouple the `tsconfig` used for typechecking and build across the
entire monorepo (as explained in point 5) to ensure ts coverage even for
files that aren't transpiled (such as scripts).
- Remove the few remaining `eslint.config.js`. I had to leave the
`richtext-lexical` and `next` ones for now. They could be moved to the
root config and scoped to their packages, as we do for example with
`templates/vercel-postgres/**`. However, I couldn't get it to work, I
don't know why.
- Make eslint in the test folder usable. Not only are we not linting
`test` in CI, but now the `pnpm eslint .` command is so large that my
computer freezes. If each suite were its own package, this would be
solved, and dynamic codegen + git hooks to modify tsconfig.base.json
wouldn't be necessary
([related](https://github.com/payloadcms/payload/pull/11984)).
### What?
Fixes issue with the Payload CLI where environment files were silently
always loaded as if in development mode, even when `NODE_ENV=production`
is explicitly set. Achieved by dynamically checking the enviroment based
on `process.env.NODE_ENV` (defaulting to "development") then passing
that to the underlying library `@next/env`.
### Why?
Previously, the Payload CLI always passed `true` to the `dev` flag of
`loadEnvConfig` from `@next/env`, causing it to load
development-specific `.env` files even when `NODE_ENV=production` was
explicitly set. Frustratingly for the user there was also no warning
message that this was happening.
For example, previously when running:
```sh
NODE_ENV=production pnpm payload run ./seed.ts
```
It would still load `.env.development*` and not `.env.production*`.
The inability to override the dev flag previously made it difficult, bar
impossible (depending on ones setup), to run the CLI in a
production-like environment. Which is useful for several reasons, a few
examples being:
- Seeding production data:
```
NODE_ENV=production payload run seed.ts
```
- Capturing current schema:
```
NODE_ENV=production payload migrate:create
```
- Running one-off jobs with live data:
```
NODE_ENV=production payload run jobs/consolidate-payments.ts
```
This fix allows users to correctly target production without surprises.
### How?
- Introduced a dev constant that checks `NODE_ENV !== 'production'`
- Passed dev to `loadEnvConfig` to allow `@next/env` to resolve the
correct `.env.*` files based on the environment:
**Before:**
```ts
const { loadedEnvFiles } = loadEnvConfig(process.cwd(), true) // assuming this won't run in production
```
**After:**
```ts
const dev = process.env.NODE_ENV !== 'production'
const { loadedEnvFiles } = loadEnvConfig(process.cwd(), dev)
```
The signature of `loadEnvConfig` from
[packages/next-env/index.ts](2086975c3c/packages/next-env/index.ts (L114))
is:
```ts
export function loadEnvConfig(
dir: string,
dev?: boolean,
log: Log = console,
forceReload = false,
onReload?: (envFilePath: string) => void
): {
combinedEnv: Env
parsedEnv: Env | undefined
loadedEnvFiles: LoadedEnvFiles
}
```
Logic from `loadEnvConfig` in
[packages/next-env/index.ts](2086975c3c/packages/next-env/index.ts (L136))
that handles loading dependant on the `dev` variable:
```ts
const mode = isTest ? 'test' : dev ? 'development' : 'production'
const dotenvFiles = [
`.env.${mode}.local`,
mode !== 'test' && `.env.local`,
`.env.${mode}`,
'.env',
]
```
This change allows Payload CLI commands to honor the current `NODE_ENV`,
loading `.env.production*`, as intended when running with
`NODE_ENV=production`.
No behavioral changes for existing dev users, but adds expected support
for production workflows.
---
Note:
There are a few consideration I made here.
1. Default to development if not explicitly set (I think most would
agree).
2. I haven't implemented warning for non standard `NODE_ENV` values, as
the `Next.js` cli does. For example `NODE_ENV=alpha`.
3. Extension of point 2, I haven't implemented loading of non-standard
`NODE_ENV` values.
I do believe either point 2 or 3 should be implemented however. So users
are not left surprised by the actions of the CLI.
FYI, the `Next.js` cli does the warning in the main package and not
`@next/env`. This logic exists in
[packages/next/src/bin/next.ts](2086975c3c/packages/next/src/bin/next.ts (L64)):
```ts
const standardEnv = ['production', 'development', 'test']
if (process.env.NODE_ENV) {
const isNotStandard = !standardEnv.includes(process.env.NODE_ENV)
const shouldWarnCommands =
process.env.NODE_ENV === 'development'
? ['start', 'build']
: process.env.NODE_ENV === 'production'
? ['dev']
: []
if (isNotStandard || shouldWarnCommands.includes(commandName)) {
warn(NON_STANDARD_NODE_ENV)
}
}
```
This warns when using the wrong `NODE_ENV` for a command (I don't think
that applies here?). Also warning when a non-standard `NODE_ENV` is used
(e.g., `NODE_ENV=alpha`) and will not attempt to load `.env.alpha*`.
This makes unexpected behaviour non-silent.
If desired, I can add the warning to this PR or follow up with a
separate PR. However, loading non-standard `NODE_ENV` is a bigger
discussion and implementation. That I would prefer to leave out of this
PR so not to block it moving along. But I felt it was worth mentioning.
⚠️ `orderable` fields will no longer be `required` and `unique`, so your
database may prompt you to accept an automatic migration if you're using
[this
feature](https://payloadcms.com/docs/configuration/collections#config-options).
Note that the `orderable` feature is still experimental, so it may still
receive breaking changes without a major upgrade or contain bugs. Use it
with caution.
___
The `orderable` fields will not have `required` and `unique` constraints
at the database schema level, in order to automatically migrate
collections that incorporate this property.
Now, when a user adds the `orderable` property to a collection or join
field, existing documents will have the order field set to undefined.
The first time you try to reorder them, the documents will be
automatically assigned an initial order, and you will be prompted to
refresh the page.
We believe this provides a better development experience than having to
manually migrate data with a script.
Additionally, it fixes a bug that occurred when using `orderable` in
conjunction with groups and tabs fields.
Closes:
- #12129
- #12331
- #12212
---------
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
When running the Payload admin panel on a machine with a slower CPU,
form state lags significantly and can become nearly unusable or even
crash when interacting with the document's `useAsTitle` field.
Here's an example:
https://github.com/user-attachments/assets/3535fa99-1b31-4cb6-b6a8-5eb9a36b31b7
#### Why this happens
The reason for this is that entire React component trees are
re-rendering on every keystroke of the `useAsTitle` field, twice over.
Here's a breakdown of the flow:
1. First, we dispatch form state events to the form context. Only the
components that are subscribed to form state re-render when this happens
(good).
2. Then, we sync the `useAsTitle` field to the document info provider,
which lives outside the form. Regardless of whether its children need to
be aware of the document title, all components subscribed to the
document info context will re-render (there are many, including the form
itself).
Given how far up the rendering tree the document info provider is, its
rendering footprint, and the rate of speed at which these events are
dispatched, this is resource intensive.
#### What is the fix
The fix is to isolate the document's title into it's own context. This
way only the components that are subscribed to specifically this context
will re-render as the title changes.
Here's the same test with the same CPU throttling, but no lag:
https://github.com/user-attachments/assets/c8ced9b1-b5f0-4789-8d00-a2523d833524
I couldn't find much information on the internet about
`__NEXT_PRIVATE_ORIGIN`, but I could observe that when port 3000 was
busy and 3001 was used, `NEXT_PUBLIC_SERVER_URL` was
`http://localhost:3000`, while `__NEXT_PRIVATE_ORIGIN` was
`http://localhost:3001`.
Fixes#12431
### What?
Basically an unnamed group moves all of its children to the same level
with the group. When another field at the same level has a unique access
setting, the permissions will return a json of permissions for each
fields at the same level instead of return a default `true` value. For
traditional group field, there will be a `fields` property inside the
permissions object, so it can use ```permissions={permissions === true ?
permissions : permissions?.fields``` as the attribution of
<RenderFields> in `packages/ui/src/fields/Group/index.tsx`. Right now,
since we somehow "promote" the group's children to the upper level,
which makes the `fields` property no longer exists in the `permissions`
object. Hence, the `permissions?.fields` mentioned above will always be
undefined, which will lead to return null for this field, because the
getFieldPermissions will always get read permission as undefined.
### Why?
The only reason we use `permissions : permissions?.fields` before
because the traditional group field moves all its children to a child
property `fields`. Since we somehow promoted those children to upper
level, so there is no need to access the fields property anymore.
### How?
For the permissions attribute for unnamed group's <RenderFields>, simple
pass in `permissions={permissions}` instead of `{permissions === true ?
permissions : permissions?.fields}`, since you have already gotten all
you want in permissions. No worry about the extra permission property
brought in(the access permission in the unnamed group level), because
`getFieldPermissions` will filter those redundant ones out.
Fixes#12430
🤖 Automated bump of templates for v3.38.0
Triggered by user: @paulpopus
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
The automated PR will override this config in other templates, so I'm
just copying it into the base template eslint config
```
{
ignores: ['.next/'],
},
```
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
`Auto-generate` button of Meta image doesn't work
### Why?
`/plugin-seo/generate-image` return imageId as below when using
`genImageResponse.text()`.
"\"result\":\"68139a9d0effac229865fbc9\""
### How?
Change `text()` to `json()` to parse the response.
Previously, `hidden: true` on a virtual field that references a
relationship field didn't work. Now, this field doesn't get calculated
if there's `hidden: true` and no `showHiddenFields` was passed.
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
Continuation of https://github.com/payloadcms/payload/pull/12185 and fix
https://github.com/payloadcms/payload/issues/12221
The mentioned PR introduced auto sorting by the point field when a
`near` query is used, but it didn't build actual needed query to order
results by their distance to a _given_ (from the `near` query) point.
Now, we build:
```sql
order by pont_field <-> ST_SetSRID(ST_MakePoint(lng, lat), 4326)
```
Which does what we want
Previously the value of new tab checkbox in the link feature was not
able to be set to true by default because we were passing `false` as a
default value.
This fixes that and adds test coverage for customising that link drawer.
This fixes issues identified in the predefined migration for
postgres v2-v3 including the following:
### relation already exists
Can error with the following:
```ts
{
err: [DatabaseError],
msg: 'Error running migration 20250502_020052_relationships_v2_v3 column "relation_id" of relation "table_name" already exists.'
}
```
This was happening when you run a migration with both a required
relationship or upload field and no schema specified in the db adapter.
When both of these are true the function that replaces `ADD COLUMN` and
`ALTER COLUMN` in order to add `NOT NULL` constraints for requried
fields, wasn't working. This resulted in the `ADD COLUMN` statement from
being being called multiple times instead of altering it after data had
been copied over.
### camelCase column change
Enum columns from using `select` or `radio` have changed from camelCase
to snake case in v3. This change was not accounted for in the
relationship migration and needed to be accounted for.
### DROP CONSTRAINT
It was pointed out by
[here](https://github.com/payloadcms/payload/issues/10162#issuecomment-2610018940)
that the `DROP CONSTRAINT` needs to include `IF EXISTS` so that it can
continue if the contraint was already removed in a previous statement.
fixes https://github.com/payloadcms/payload/issues/10162
Fixes the issue where this returns all the documents:
```
{
name: 'post',
type: 'relationship',
relationTo: 'posts',
filterOptions: { id: { in: [] } }
}
```
The issue isn't with the Local API but with how we send the query to the
REST API through `qs.stringify`. `qs.stringify({ id: { in: [] } }`
becomes `""`, so the server ignores the original query. I don't think
it's possible to encode empty arrays with this library
https://github.com/sindresorhus/query-string/issues/231, so I just made
sanitization to `{ exists: false }` for this case.
Previously, plugin-cloud would only set up job auto-running if a job configuration was present in the custom config at initialization time.
However, some jobs - such as the scheduled publish job which is added during sanitization - are added after plugin-cloud has initialized. This means relying solely on the initial state of the job config is insufficient for determining whether to enable auto-running.
This PR removes that check and ensures auto-running is always initialized, allowing later-added jobs to run as expected.
## Weakening type
This PR also weakens to `config.jobs.tasks` type and makes that property optional. It's totally permissible to only have workflows that define inline tasks, and to not have any static tasks defined in `config.jobs.tasks`. Thus it makes no sense to make that property required.
Optimizes the field sanitization process by removing duplicative deep
loops over the config. We were previously iterating over all fields of
each collection potentially multiple times in order validate field
configs, check reserved field names, etc. Now, we perform all necessary
sanitization within a single loop.
Prevents an accidental lockout of query preset documents. An "accidental
lockout" occurs when the user sets access control on a preset and
excludes themselves. This can happen in a variety of scenarios,
including:
- You select `specificUsers` without specifying yourself
- You select `specificRoles` without specifying a role that you are a
part of
- Etc.
#### How it works
To make this happen, we use a custom validation function that executes
access against the user's proposed changes. If those changes happen to
remove access for them, we throw a validation error and prevent that
change from ever taking place. This means that only a user with proper
access can remove another user from the preset. You cannot remove
yourself.
To do this, we create a temporary record in the database that we can
query against. We use transactions to ensure that the temporary record
is not persisted once our work is completed. Since not all Payload
projects have transactions enabled, we flag these temporary records with
the `isTemp` field.
Once created, we query the temp document to determine its permissions.
If any of the operations throw an error, this means the user can no
longer act on them, and we throw a validation error.
#### Alternative Approach
A previous approach that was explored was to add an `owner` field to the
presets collection. This way, the "owner" of the preset would be able to
completely bypass all access control, effectively eliminating the
possibility of a lockout event.
But this doesn't work for other users who may have update access. E.g.
they could still accidentally remove themselves from the read or update
operation, preventing them from accessing that preset after submitting
the form. We need a solution that works for all users, not just the
owner.
### What
This PR introduces a comprehensive customization system for toolbar
groups in the Lexical Rich Text Editor. It allows developers to override
not just the order, but virtually any aspect of toolbar components (such
as format, align, indent) through the `FixedToolbarFeature`
configuration. Customizable properties include order, icons, group type,
and more.
### Why
Previously, toolbar group configurations were hardcoded in their
respective components with no way to modify them without changing the
source code. This made it difficult for developers to:
1. Reorder toolbar components to match specific UX requirements
2. Replace icons with custom ones to maintain design consistency
3. Transform dropdown groups into button groups or vice versa
4. Apply other customizations needed for specific projects
This enhancement provides full flexibility for tailoring the rich text
editor interface while maintaining a clean and maintainable codebase.
### How
The implementation consists of three key parts:
1. **Enhanced the FixedToolbarFeature API**:
- Added a new `customGroups` property to `FixedToolbarFeatureProps` that
accepts a record mapping group keys to partial `ToolbarGroup` objects
- These partial objects can override any property of the default toolbar
group configuration
2. **Leveraged existing deep merge utility**:
- Used Payload's existing `deepMerge` utility to properly combine
default configurations with custom overrides
- This ensures that only specified properties are overridden while
preserving all other default behaviors
3. **Applied customizations in the sanitization process**:
- Updated the `sanitizeClientFeatures` function to identify and apply
custom group configurations
- Applied deep merging before the sorting process to ensure proper
ordering with customized configurations
- Maintained backward compatibility for users who don't need
customization
### Usage Example
```typescript
import { FixedToolbarFeature } from '@payloadcms/richtext-lexical'
import { CustomIcon } from './icons/CustomIcon'
{
name: 'content',
type: 'richText',
admin: {
features: [
// Other features...
FixedToolbarFeature({
customGroups: {
'text': {
order: 10,
ChildComponent: CustomIcon,
},
'format': {
order: 15,
},
'add': {
type: 'buttons',
order: 20,
},
}
})
]
}
}
```
### Demo
https://github.com/user-attachments/assets/c3a59b60-b6c2-4721-bbc0-4954bdf52625
---------
Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
Threads the `overrideAccess` property through the field-level
validations. This way custom `validate` functions can be aware of its
value and adjust their logic accordingly.
See #12322 for an example use case.
Follow-up work to #12046, which was misnamed. It improved UI
responsiveness of the rich text field on CPU-limited clients, but didn't
actually reduce work by debouncing. It only improved scheduling.
Using `requestIdleCallback` lead to better scheduling of change event
handling in the rich text editor, but on CPU-starved clients, this leads
to a large backlog of unprocessed idle callbacks. Since idle callbacks
are called by the browser in submission order, the latest callback will
be processed last, potentially leading to large time delays between a
user typing, and the form state having been updated. An example: When a
user types "I", and the change events for the character "I" is scheduled
to happen in the next browser idle time, but then the user goes on to
type "love Payload", there will be 12 more callbacks scheduled. On a
slow system it's preferable if the browser right away only processes the
event that has the full editor state "I love Payload", instead of only
processing that after 11 other idle callbacks.
So this code change keeps track when requesting an idle callback and
cancels the previous one when a new change event with an updated editor
state occurs.
### What?
Turborepo fails to compile due to type error in the generated drizzle
schema.
### Why?
TypeScript may not include the module augmentation for
@payloadcms/db-postgres, especially in monorepo or isolated module
builds. This causes type errors during the compilation process of
turborepo project. Adding the type-only import guarantees that
TypeScript loads the relevant type definitions and augmentations,
resolving these errors.
### How?
This PR adds a type-only import statement to ensure TypeScript
recognizes the module augmentation for @payloadcms/db-postgres in the
generated drizzle schema from payload, and there is no runtime effect.
Fixes#12311
-->

The official playwright extension when using the debug button to run
tests in debug mode doesn't pick up the `tests/test.env` file as
expected.
I've added the same `NODE_OPTIONS` to the vscode settings JSON for this
extension which fixes an error when running e2e tests in debug mode.
### What?
Allows the field value (if defined) to be accessed from `args` with
custom server components.
### Why?
Documentation states that the user can access `args.value` to get the
value of the field at time of render (if a value is defined) when using
a custom server component - however this isn't currently setup.
<img width="469" alt="Screenshot 2025-05-08 at 4 51 30 PM"
src="https://github.com/user-attachments/assets/9c167f80-5c5e-4fea-a31c-166281d9f7db"
/>
Link to docs
[here](https://payloadcms.com/docs/fields/overview#default-props).
### How?
Passes the value from `data` if it exists (does not exist for all field
types) and adds `value` to the server component types as an optional
property.
Fixes#10389
The docs for Vercel Content Link only included instructions on how to
enable content source maps for the REST API. The Local API, although
supported, was lacking documentation.
Fixes sorting by fields in relationships, e.g `sort: "author.name"` when
using `draft: true`. The existing test that includes check with `draft:
true` was accidentally passing because it used to sort by the
relationship field itself.
Fixes https://github.com/payloadcms/payload/issues/12263
This was caused by passing not needed columns to the `SELECT DISTINCT`
query, which we execute in case if we have a filter / sort by a nested
field / relationship. Since the only columns that we need to pass to the
`SELECT DISTINCT` query are: ID and field(s) specified in `sort`, we now
filter the `selectFields` variable.
This PR does two things:
- Adds a new ` --no-experimental-strip-types` flag to the playwright
test env
- This is needed since 23.6.0 automatically enables this flag by default
and it breaks e2e tests
- Bumps the tooling config files to use node 23.11.0
Fixes https://github.com/payloadcms/payload/issues/9449
Previously, search sync with categories didn't work and additionally
caused problems with Postgres. Additionally, ensures that when doing
synchronization, all the categories are populated, since we don't always
have populated data inside hooks.
### What?
Standardizes ESLint configurations across all template projects like
website template to ensure consistent code quality enforcement.
### Why?
Previously, there were inconsistencies in the ESLint configurations
between different template projects. Some templates were missing the
.next/ ignore pattern, which could lead to unnecessary linting of build
files. By standardizing these configurations, we ensure consistent code
quality standards and developer experience across all template projects.
### How?
Added the missing ignores: ['.next/'] configuration to templates that
were missing it
### What?
Swaps out `deepAssertEqual` for `dequal` package. Further details and
motivation in [this
discussion](https://github.com/payloadcms/payload/discussions/12192).
### Why?
Dequal is about 100x faster in limited local testing. Dequal package
shows 3-5x speed over `deepAssertEqual` in benchmarks. Memory usage is
within acceptable levels.
### How?
Move the result of dequal to a `const` for readability. Replace the `try
{ ... } catch { ... }` with `if { ... } else { ... }` for minimum impact
and change.
### What?
Fixes#12171
### Why?
Previously, the ImageMedia component was not properly handling URL
formatting when a serverURL was configured in Payload. This caused
images to fail to load when using a custom serverURL. By extracting the
URL handling logic into a separate utility function, we ensure
consistent URL processing across both image and video components.
### How?
1. Created a new utility function getMediaUrl in
`src/utilities/getMediaUrl.ts` that:
- Properly checks for HTTP/HTTPS protocols
- Handles null or undefined URL values
- Supports cache tags to prevent caching issues
- Uses `getClientSideURL()` for relative paths
2. Updated the ImageMedia component to use this utility function instead
of inline URL processing logic
3. Updated the VideoMedia component to also use the same utility
function for consistency
### What?
If an error occurs while unpublishing a document in the edit view UI,
the toast which shows the error message now displays the actual message
which is sent from the server, if available.
### Why?
Only a generic error message was shown if an unpublish operation failed.
Some errors might be solvable by the user, so that there is value in
showing the actual, actionable error message instead of a generic one.
### How?
The server response is parsed for error message if an unpublish
operation fails and displayed in the toast, instead of the generic error
message.

Adds pre-signed URLs support file downloads with the S3 adapter. Can be
enabled per-collection:
```ts
s3Storage({
collections: {
media: { signedDownloads: true }, // or { signedDownloads: { expiresIn: 3600 }} for custom expiresIn (default 7200)
},
bucket: process.env.S3_BUCKET,
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
endpoint: process.env.S3_ENDPOINT,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
region: process.env.S3_REGION,
},
}),
```
The main use case is when you care about the Payload access control (so
you don't want to use `disablePayloadAccessControl: true` but you don't
want your files to be served through Payload (which can affect
performance with large videos for example).
This feature instead generates a signed URL (after verifying the access
control) and redirects you directly to the S3 provider.
This is an addition to https://github.com/payloadcms/payload/pull/11382
which added pre-signed URLs for file uploads.
### What?
Extract text from the React node label in WhereBuilder
### Why?
If you have a nested field in filter options, the label would show
correctly, but the search will not work
### How
By adding an `extractTextFromReactNode` function that gets text out of
React.node label
### Code setup:
```
{
type: "collapsible",
label: "Meta",
fields: [
{
name: 'media',
type: 'relationship',
relationTo: 'media',
label: 'Ferrari',
filterOptions: () => {
return {
id: { in: ['67efdbc872ca925bc2868933'] },
}
}
},
{
name: 'media2',
type: 'relationship',
relationTo: 'media',
label: 'Williams',
filterOptions: () => {
return {
id: { in: ['67efdbc272ca925bc286891c'] },
}
}
},
],
},
```
### Before:
https://github.com/user-attachments/assets/25d4b3a2-6ac0-476b-973e-575238e916c4
### After:
https://github.com/user-attachments/assets/92346a6c-b2d1-4e08-b1e4-9ac1484f9ef3
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
### What?
Tenant Selector doesn’t honor the custom order when ‘orderable’ is
enabled for Tenant collection
### Why?
Currently, it uses "useAsTitle" to sort. In some use cases, for example,
when a user manages multiple tenants that have an inherent priority
(such as usage frequency), sorting purely by the useAsTitle isn’t very
practical.
### How?
Get "orderable" config from the tenant collection's config, if it has
"orderable" set as true, it will use _order to sort. If not, it will use
"useAsTitle" to sort as default.
Fixes#12246

## Fix
We were able to narrow it down to this call
816fb28f55/packages/plugin-cloud-storage/src/utilities/getFilePrefix.ts (L26-L41)
Adding `draft: true` fixes the issue. It seems that the `prefix` can
only be found in a draft, and without `draft: true` those drafts aren't
searched.
### Issue reproduction
In the community folder, enable versioning for the media collection and
install the `s3storage` plugin (see Git patch). I use `minio` to have a
local S3 compatible backend and then I run the app with:
`AWS_ACCESS_KEY_ID=minioadmin AWS_SECRET_ACCESS_KEY=minioadmin
START_MEMORY_DB=true pnpm dev _community`.
Next, open the media collection and create a new entry. Then open that
entry, remove the file it currently has, and upload a new file. Save as
draft.
Now the media can no longer be accessed and the thumbnails are broken.
If you make an edit but save it by publishing the issue goes away. I
also couldn't reproduce this by adding a text field, changing that, and
saving the document as draft.
```diff
diff --git test/_community/collections/Media/index.ts test/_community/collections/Media/index.ts
index bb5edd0349..689423053c 100644
--- test/_community/collections/Media/index.ts
+++ test/_community/collections/Media/index.ts
@@ -9,6 +9,9 @@ export const MediaCollection: CollectionConfig = {
read: () => true,
},
fields: [],
+ versions: {
+ drafts: true,
+ },
upload: {
crop: true,
focalPoint: true,
diff --git test/_community/config.ts test/_community/config.ts
index ee1aee6e46..c81ec5f933 100644
--- test/_community/config.ts
+++ test/_community/config.ts
@@ -7,6 +7,7 @@ import { devUser } from '../credentials.js'
import { MediaCollection } from './collections/Media/index.js'
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
import { MenuGlobal } from './globals/Menu/index.js'
+import { s3Storage } from '@payloadcms/storage-s3'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -24,6 +25,21 @@ export default buildConfigWithDefaults({
// ...add more globals here
MenuGlobal,
],
+ plugins: [
+ s3Storage({
+ enabled: true,
+ bucket: 'amboss',
+ config: {
+ region: 'eu-west-1',
+ endpoint: 'http://localhost:9000',
+ },
+ collections: {
+ media: {
+ prefix: 'media',
+ },
+ },
+ }),
+ ],
onInit: async (payload) => {
await payload.create({
collection: 'users',
```
## Screen recording
https://github.com/user-attachments/assets/b13be4a3-e858-427a-8bfa-6592b87748ee
Exports a few utilities that are used internally to the login operation,
but could be helpful for others building plugins.
Specifically:
- `isUserLocked` - a check to ensure that a given user is not locked due
to too many invalid attempts
- `checkLoginPermissions` - checks to see that the user is not locked as
well as that it is properly verified, if applicable
- `jwtSign` - Payload's internal JWT signing approach
- `getFieldsToSign` - reduce down a document's fields for JWT creation
based on collection config settings
- `incrementLoginAttempts` / `resetLoginAttempts` - utilities to handle
both failed and successful login attempts
- `UnverifiedEmail` - an error that could be thrown if attempting to log
in to an account without prior successful email verification
### What?
Extends trigger of a reload of the fields for RelationshipFilter to
include `filterOptions`.
### Why?
If you have two or more relationship fields that have a relation to the
same collection, the options of the filter will not update.
### How
By extending dependencies of `useEffect`
### Code setup:
```
{
name: 'media',
type: 'relationship',
relationTo: 'media',
filterOptions: () => {
return {
id: { in: ['67efaee24648d01dffceecf9'] },
}
}
},
{
name: 'media2',
type: 'relationship',
relationTo: 'media',
filterOptions: () => {
return {
id: { in: ['67efafb04648d01dffceed75'] },
}
}
},
```
### Before:
https://github.com/user-attachments/assets/bdc5135b-3afa-48df-98fe-6a9153dd7710
### After:
https://github.com/user-attachments/assets/d71a7558-6413-4c97-9b0b-678cf3b011d0
-->
### What?
Adds an option to open the current document in a new tab when opened in
a drawer.
### Why?
There is currently no direct way to open a document when opened in a
drawer. However, sometimes editors want to edit one or multiple
documents from relationships independently of the current edit view and
need an easy option to open these separately.
### How?
Converts the document id to a link if in drawer context.

---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
When running the v2-v3 migration you might receive prompts for renaming
columns. Since we start a transaction before, you might end up with a
fail if you don't answer within your transaction session period timeout.
This moves the `getTransaction` call after prompts were answered, since
we don't have a reason to start it earlier.
This PR introduced https://github.com/payloadcms/payload/pull/11952
improvement for graphql schema with making fields of the `Paginated<T>`
interface non-nullable.
However, there are a few special ones - `nextPage` and `prevPage`. They
can be `null` when:
The result returned 0 docs.
The result returned `x` docs, but in the DB we don't have `x+1` doc.
Thus, `nextPage` will be `null`. The result will have `nextPage: null`.
Finally, when we query 1st page, `prevPage` is `null` as well.
<img width="873" alt="image"
src="https://github.com/user-attachments/assets/04d04b13-ac26-4fc1-b421-b5f86efc9b65"
/>
When `payload migrate` is run and a record with name "dev" is returned
having `batch: -1`, then the `batch` is not incrementing as expected as
it is stuck at 1. This change makes it so the batch is incremented from
the correct latest batch, ignoring the `name: "dev"` migration.
### What?
Adds line-breaks after headings, lists, list items, tables, table rows,
and table cells when converting lexical content to plaintext.
### Why?
Currently text from those nodes is concatenated without a separator.
### How?
Adds handling for these nodes to the plain text converter.
### What?
Selected documents in a relationship field can be opened in a new tab.
### Why?
Related documents can be edited using the edit icon which opens the
document in a drawer. Sometimes users would like to open the document in
a new tab instead to e.g. modify the related document at a later point
in time. This currently requires users to find the related document via
the list view and open it there. There is no easy way to find and open a
related document.
### How?
Adds custom handling to the relationship edit button to support opening
it in a new tab via middle-click, Ctrl+click, or right-click → 'Open in
new tab'.
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
In this case, the `blockType` property is created on the server, but -
prior to this fix - was discarded on the client in
[`fieldReducer.ts`](https://github.com/payloadcms/payload/blob/main/packages/ui/src/forms/Form/fieldReducer.ts#L186-L198)
via
[`mergerServerFormState.ts`](b9832f40e4/packages/ui/src/forms/Form/mergeServerFormState.ts (L29-L31)),
because the field's path neither existed in the client's form state, nor
was it marked as `addedByServer`.
This caused later calls to POST requests to form state to send without
the `blockType` key for block rows, which in turn caused
`addFieldStatePromise.ts` to throw the following error:
```
Block with type "undefined" was found in block data, but no block with that type is defined in the config for field with schema path ${schemaPath}.
```
This prevented the client side form state update from completing, and if
the form state was saved, broke the document.
This is a follow-up to #12131, which treated the symptom, but not the
cause. The original issue seems to have been introduced in
https://github.com/payloadcms/payload/releases/tag/v3.34.0. It's unclear
to me whether this issue is connected to block E2E tests having been
disabled in the same release in
https://github.com/payloadcms/payload/pull/11988.
## How to reproduce
### Collection configuration
```ts
const RICH_TEXT_BLOCK_TYPE = 'richTextBlockType'
const RichTextBlock: Block = {
slug: RICH_TEXT_BLOCK_TYPE,
interfaceName: 'RichTextBlock',
fields: [
{
name: 'richTextBlockField',
label: 'Rich Text Field in Block Field',
type: 'richText',
editor: lexicalEditor({}),
required: true,
},
],
}
const MyCollection: CollectionConfig = {
slug: 'my-collection-slug,
fields: [
{
name: 'arrayField',
label: 'Array Field',
type: 'array',
fields: [
{
name: 'blockField',
type: 'blocks',
blocks: [RichTextBlock],
required: true,
},
],
},
]
}
export default MyCollection
```
### Steps
- Press "Add Array Field"
--> ✅ 1st block with rich text is added
- Press "Add Array Field" a 2nd time
### Result
- 🛑 2nd block is indefinitely in loading state (side-note: the form UI
should preferably explicitly indicate the error).
- 🛑 If saving the document, it is corrupted and will only show a blank
page (also not indicating any error).
Client side:
<img width="1268" alt="Untitled"
src="https://github.com/user-attachments/assets/4b32fdeb-af76-41e2-9181-d2dbd686618a"
/>
API error:
<img width="1272" alt="image"
src="https://github.com/user-attachments/assets/35dc65f7-88ac-4397-b8d4-353bcf6a4bfd"
/>
Client side, when saving and re-opening document (API error of `GET
/admin/collections/${myCollection}/${documentId}` is the same (arguably
the HTTP response status code shouldn't be `200`)):
<img width="1281" alt="image"
src="https://github.com/user-attachments/assets/2e916eb5-6f10-4e82-9b84-1dc41db21d47"
/>
### Result after fix
- `blockType` is sent from the client to the server.
- ✅ 2nd block with rich text is added.
- ✅ Document does not break when saving & re-opening.
<img width="1277" alt="Untitled"
src="https://github.com/user-attachments/assets/84d0c88b-64b2-48c4-864d-610d524ac8fc"
/>
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
### What?
Fixes the label for documents which were the current published document
but got unpublished in the version view.
### Why?
If the most recent published document was unpublished, it remained
displayed as "Currently published version" in the version list.
### How?
Checks whether the document has a currently published version instead of
only looking at the latest published version when determining the label
in the versions view.
Fixes https://github.com/payloadcms/payload/issues/10838
---------
Co-authored-by: Alessio Gravili <alessio@gravili.de>
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
### What?
Fix link to "Blank Template" in installation.mdx so that it displays
correctly on the web.
### Why?
Text of broken md link looks bad.
### How?
Remove angle brackets.
### Fixes:

### What?
Allows array fields to be filtered in the list view.
### Why?
Array fields were not filterable in the list view although all other
field types were filterable already.
### How?
Adds handling for array fields as filter option.

Fixes population of joins that target relationship fields that have
`relationTo` as an array, for example:
```ts
// Posts collection
{
name: 'polymorphic',
type: 'relationship',
relationTo: ['categories', 'users'],
},
// Categories collection
{
name: 'polymorphic',
type: 'join',
collection: 'posts',
on: 'polymorphic',
}
```
Thanks @jaycetde for the integration test
https://github.com/payloadcms/payload/pull/12278!
---------
Co-authored-by: Jayce Pulsipher <jpulsipher@nav.com>
### What?
Using `create-payload-app` to initialize Payload in an existing Next.js
app **that does not already have Payload installed** overwrites any
existing data in the `.env` and `.env.example` files.
The desired behavior is for Payload variables to get added with no
client data lost.
### How?
Updates `manageEnvFiles` to check for existing `.env / .env.example`
file and appends or creates as necessary.
Adds tests to
`packages/create-payload-app/src/lib/create-project.spec.ts`.
#### Fixes https://github.com/payloadcms/payload/issues/10355
2025-05-01 16:03:07 +00:00
2477 changed files with 118176 additions and 37319 deletions
description:Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
render:text
placeholder:|
Payload:
Node.js:
Next.js:
placeholder:Run `pnpm payload info` in your terminal and paste the output here.
@@ -40,7 +40,7 @@ There are a couple ways run integration tests:
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
@@ -57,7 +57,7 @@ The easiest way to run E2E tests is to install
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
- Packages are located in the `packages/` directory.
- The main Payload package is `packages/payload`. This contains the core functionality.
- Database adapters are in `packages/db-*`.
- The UI package is `packages/ui`.
- The Next.js integration is in `packages/next`.
- Rich text editor packages are in `packages/richtext-*`.
- Storage adapters are in `packages/storage-*`.
- Email adapters are in `packages/email-*`.
- Plugins which add additional functionality are in `packages/plugin-*`.
- Documentation is in the `docs/` directory.
- Monorepo tooling is in the `tools/` directory.
- Test suites and configs are in the `test/` directory.
- LLMS.txt is at URL: https://payloadcms.com/llms.txt
- LLMS-FULL.txt is at URL: https://payloadcms.com/llms-full.txt
## Dev environment tips
- Any package can be built using a `pnpm build:*` script defined in the root `package.json`. These typically follow the format `pnpm build:<directory_name>`. The options are all of the top-level directories inside the `packages/` directory. Ex `pnpm build:db-mongodb` which builds the `packages/db-mongodb` package.
- ALL packages can be built with `pnpm build:all`.
- Use `pnpm dev` to start the monorepo dev server. This loads the default config located at `test/_community/config.ts`.
- Specific dev configs for each package can be run with `pnpm dev <directory_name>`. The options are all of the top-level directories inside the `test/` directory. Ex `pnpm dev fields` which loads the `test/fields/config.ts` config. The directory name can either encompass a single area of functionality or be the name of a specific package.
## Testing instructions
- There are unit, integration, and e2e tests in the monorepo.
- Unit tests can be run with `pnpm test:unit`.
- Integration tests can be run with `pnpm test:int`. Individual test suites can be run with `pnpm test:int <directory_name>`, which will point at `test/<directory_name>/int.spec.ts`.
- E2E tests can be run with `pnpm test:e2e`.
- All tests can be run with `pnpm test`.
- Prefer running `pnpm test:int` for verifying local code changes.
## PR Guidelines
- This repository follows conventional commits for PR titles
- PR Title format: <type>(<scope>): <title>. Title must start with a lowercase letter.
- Valid types are build, chore, ci, docs, examples, feat, fix, perf, refactor, revert, style, templates, test
- Prefer `feat` for new features and `fix` for bug fixes.
- Scopes should be chosen based upon the package(s) being modified. If multiple packages are being modified, choose the most relevant one or no scope at all.
- Example PR titles:
-`feat(db-mongodb): add support for transactions`
-`feat(richtext-lexical): add options to hide block handles`
-`fix(ui): json field type ignoring editorOptions`
## Commit Guidelines
- This repository follows conventional commits for commit messages
- The first commit of a branch should follow the PR title format: <type>(<scope>): <title>. Follow the same rules as PR titles.
- Subsequent commits should prefer `chore` commits without a scope unless a specific package is being modified.
- These will eventually be squashed into the first commit when merging the PR.
@@ -87,41 +87,43 @@ You can run the entire test suite using `pnpm test`. If you wish to only run e2e
By default, `pnpm test:int` will only run int test against MongoDB. To run int tests against postgres, you can use `pnpm test:int:postgres`. You will have to have postgres installed on your system for this to work.
### Commits
### Pull Requests
We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for our commit messages. Please follow this format when creating commits. Here are some examples:
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR description.
-`feat: adds new feature`
-`fix: fixes bug`
-`docs: adds documentation`
-`chore: does chore`
All commits within a PR are squashed when merged, using the PR title as the commit message. For that reason, please use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for your PR titles.
Here's a breakdown of the format. At the top-level, we use the following types to categorize our commits:
Here are some examples:
-`feat`: new feature that adds functionality. These are automatically added to the changelog when creating new releases.
-`fix`: a fix to an existing feature. These are automatically added to the changelog when creating new releases.
-`docs`: changes to [docs](./docs) only. These do not appear in the changelog.
-`chore`: changes to code that is neither a fix nor a feature (e.g. refactoring, adding tests, etc.). These do not appear in the changelog.
-`feat: add new feature`
-`fix: fix bug`
-`docs: add documentation`
-`test: add/fix tests`
-`refactor: refactor code`
-`chore: anything that does not fit into the above categories`
If applicable, you must indicate the affected packages in parentheses to "scope" the changes. Changes to the payload chore package do not require scoping.
Here are some examples:
-`feat(ui): add new feature`
-`fix(richtext-lexical): fix bug`
If you are committing to [templates](./templates) or [examples](./examples), use the `chore` type with the proper scope, like this:
-`chore(templates): adds feature to template`
-`chore(examples): fixes bug in example`
## Pull Requests
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR message.
## Previewing docs
This is how you can preview changes you made locally to the docs:
3. Duplicate the `.env.example` file and rename it to `.env`
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)
5. Run `pnpm fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
6. You're done! Now you can start the website locally using `pnpm dev` and preview the docs under [http://localhost:3000/docs/local](http://localhost:3000/docs/local)
@@ -45,7 +45,7 @@ There are a couple ways to do this:
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
@@ -62,7 +62,7 @@ The easiest way to run E2E tests is to install
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
@@ -64,7 +64,7 @@ If a Global supports [Versions](../versions/overview), the following additional
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can read this global based on its current properties.
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../configuration/globals):
To add read Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
```ts
import { GlobalConfig } from 'payload'
@@ -72,7 +72,7 @@ import { GlobalConfig } from 'payload'
| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
| **`buildPath`** | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
| **`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). |
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
| **`timezones`** | Configure the timezone settings for the admin panel. [More details](#timezones) |
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
| `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). |
| `autoRefresh` | Used to automatically refresh user tokens for users logged into the dashboard. [More details](../authentication/overview). |
| `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). |
| `meta` | Base metadata to use for the Admin Panel. [More details](./metadata). |
| `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
| `suppressHydrationWarning` | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
| `theme` | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
| `timezones` | Configure the timezone settings for the admin panel. [More details](#timezones) |
| `toast` | Customize the handling of toast messages within the Admin Panel. [More details](#toasts) |
| `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
<Banner type="success">
**Reminder:** These are the _root-level_ options for the Admin Panel. You can
@@ -187,6 +189,12 @@ The following options are available:
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
| `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. |
<Banner type="warning">
**Important:** Changing Root-level Routes also requires a change to [Project
Structure](#project-structure) to match the new route. [More
details](#customizing-root-level-routes).
</Banner>
<Banner type="success">
**Tip:** You can easily add _new_ routes to the Admin Panel through [Custom
Endpoints](../rest-api/overview#custom-endpoints) and [Custom
@@ -197,13 +205,29 @@ The following options are available:
You can change the Root-level Routes as needed, such as to mount the Admin Panel at the root of your application.
Changing Root-level Routes also requires a change to [Project Structure](#project-structure) to match the new route. For example, if you set `routes.admin` to `/`, you would need to completely remove the `admin` directory from the project structure:
This change, however, also requires a change to your [Project Structure](#project-structure) to match the new route.
For example, if you set `routes.admin` to `/`:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
routes: {
admin: '/', // highlight-line
},
})
```
Then you would need to completely remove the `admin` directory from the project structure:
```plaintext
app/
├─ (payload)/
├── [[...segments]]/
app
├─ (payload)
├── [[...segments]]
├──── ...
├── layout.tsx
```
<Banner type="warning">
@@ -276,3 +300,20 @@ We validate the supported timezones array by checking the value against the list
`timezone: true`. See [Date Fields](../fields/overview#date) for more
information.
</Banner>
## Toast
The `admin.toast` configuration allows you to customize the handling of toast messages within the Admin Panel, such as increasing the duration they are displayed and limiting the number of visible toasts at once.
<Banner type="info">
**Note:** The Admin Panel currently uses the
[Sonner](https://sonner.emilkowal.ski) library for toast notifications.
</Banner>
The following options are available for the `admin.toast` configuration:
The Preview feature can be used to achieve "Draft Preview". After clicking the preview button from the Admin Panel, you can enter into "draft mode" within your front-end application. This will allow you to adjust your page queries to include the `draft: true` param. When this param is present on the request, Payload will send back a draft document as opposed to a published one based on the document's `_status` field.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here very from framework to framework although the underlying concept is the same.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here vary from framework to framework although the underlying concept is the same.
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Your component will re-render when _any_ field changes, so use this hook only if you absolutely need to. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
<Banner type="warning">
**Warning:** Your component will re-render when _any_ field changes, so use
this hook only if you absolutely need to.
</Banner>
You can do lots of powerful stuff by retrieving the full form state, like using built-in helper functions to reduce field state to values only, or to retrieve sibling data by path.
@@ -605,7 +610,7 @@ return (
textField: {
initialValue: 'Updated text',
valid: true,
value: 'Upddated text',
value: 'Updated text',
},
},
// blockType: "yourBlockSlug",
@@ -734,7 +739,7 @@ The `useDocumentInfo` hook provides information about the current document being
| **`lastUpdateTime`** | Timestamp of the last update to the document. |
| **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. |
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](./preferences). |
| **`savedDocumentData`** | The saved data of the document. |
| **`data`** | The saved data of the document. |
| **`setDocFieldPreferences`** | Method to set preferences for a specific field. [More details](./preferences). |
| **`setDocumentTitle`** | Method to set the document title. |
| **`setHasPublishedDoc`** | Method to update whether the document has been published. |
@@ -875,7 +880,7 @@ Useful to retrieve info about the currently logged in user as well as methods fo
| **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token |
| **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory |
| **`token`** | The logged in user's token (useful for creating preview links, etc.) |
| **`refreshPermissions`** | Load new permissions (useful when content that effects permissions has been changed) |
| **`refreshPermissions`** | Load new permissions (useful when content that affects permissions has been changed) |
| **`permissions`** | The permissions of the current user |
| **`columns`** | The current state of columns including their active status and configuration |
| **`LinkedCellOverride`** | A component override for linked cells in the table |
| **`moveColumn`** | A method to reorder columns. Accepts `{ fromIndex: number, toIndex: number }` as arguments |
| **`resetColumnsState`** | A method to reset columns back to their default configuration as defined in the collection config |
| **`setActiveColumns`** | A method to set specific columns to active state while preserving the existing column order. Accepts an array of column names to activate |
| **`toggleColumn`** | A method to toggle a single column's visibility. Accepts a column name as string |
```tsx
'use client'
@@ -989,17 +1002,30 @@ import { useTableColumns } from '@payloadcms/ui'
@@ -1143,7 +1169,7 @@ This is useful for scenarios where you need to trigger another fetch regardless
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.
By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions by default.
@@ -62,7 +62,7 @@ In this scenario, if your cookie was still valid, malicious-intent.com would be
### CSRF Prevention
Define domains that your trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this:
Define domains that you trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this:
```ts
// payload.config.ts
@@ -102,8 +102,8 @@ If option 1 isn't possible, then you can get around this limitation by [configur
```
SameSite: None // allows the cookie to cross domains
Secure: true // ensures its sent over HTTPS only
HttpOnly: true // ensures its not accessible via client side JavaScript
Secure: true // ensures it's sent over HTTPS only
HttpOnly: true // ensures it's not accessible via client side JavaScript
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML.
```ts
import type { CollectionConfig } from 'payload'
@@ -178,7 +178,7 @@ The following arguments are passed to the `generateEmailHTML` function:
#### generateEmailSubject
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML.
In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponse` to `true` like so:
In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponses` to `true` like so:
```ts
import type { CollectionConfig } from 'payload'
@@ -46,7 +49,7 @@ import type { CollectionConfig } from 'payload'
Document access can also be queried on a collection/global basis. Access on a global can queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`.
Document access can also be queried on a collection/global basis. Access on a global can be queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`.
## Me
@@ -180,19 +180,22 @@ As Payload sets HTTP-only cookies, logging out cannot be done by just removing a
**Example REST API logout**:
```ts
const res = await fetch('http://localhost:3000/api/[collection-slug]/logout', {
By default, logging out will only end the session pertaining to the JWT that was used to log out with. However, you can pass `allSessions: true` to the logout operation in order to end all sessions for the user logging out.
## Refresh
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
**Note:** Auth-enabled Collections with be automatically injected with the
**Note:** Auth-enabled Collections will be automatically injected with the
`hash`, `salt`, and `email` fields. [More
details](../fields/overview#field-names).
</Banner>
@@ -91,6 +91,7 @@ The following options are available:
| **`strategies`** | Advanced - an array of custom authentication strategies to extend this collection's authentication with. [More details](./custom-strategies). |
| **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. |
| **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](./api-keys). |
| **`useSessions`** | True by default. Set to `false` to use stateless JWTs for authentication instead of sessions. |
| **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More details](./email#email-verification). |
### Login With Username
@@ -142,14 +143,17 @@ import { buildConfig } from 'payload'
@@ -169,13 +173,32 @@ The following options are available:
| **`password`** | The password of the user to login as. This is only needed if `prefillOnly` is set to true |
| **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. |
## Auto-Refresh
Turning this property on will allow users to stay logged in indefinitely while their browser is open and on the admin panel, by automatically refreshing their authentication token before it expires.
To enable auto-refresh for user tokens, set `autoRefresh: true` in the [Payload Config](../admin/overview#admin-options) to:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
// highlight-start
admin: {
autoRefresh: true,
},
// highlight-end
})
```
## Operations
All auth-related operations are available via Payload's REST, Local, and GraphQL APIs. These operations are automatically added to your Collection when you enable Authentication. [More details](./operations).
## Strategies
Out of the box Payload ships with a three powerful Authentication strategies:
Out of the box Payload ships with three powerful Authentication strategies:
- [HTTP-Only Cookies](./cookies)
- [JSON Web Tokens (JWT)](./jwt)
@@ -198,3 +221,43 @@ API Keys can be enabled on auth collections. These are particularly useful when
### Custom Strategies
There are cases where these may not be enough for your application. Payload is extendable by design so you can wire up your own strategy when you need to. [More details](./custom-strategies).
### Access Control
Default auth fields including `email`, `username`, and `password` can be overridden by defining a custom field with the same name in your collection config. This allows you to customize the field — including access control — while preserving the underlying auth functionality. For example, you might want to restrict the `email` field from being updated once it is created, or only allow it to be read by certain user roles. You can achieve this by redefining the field and setting access rules accordingly.
Here's an example of how to restrict access to default auth fields:
```ts
import type { CollectionConfig } from 'payload'
export const Auth: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{
name: 'email', // or 'username'
type: 'text',
access: {
create: () => true,
read: () => false,
update: () => false,
},
},
{
name: 'password', // this will be applied to all password-related fields including new password, confirm password.
type: 'text',
hidden: true, // needed only for the password field to prevent duplication in the Admin panel
access: {
update: () => false,
},
},
],
}
```
**Note:**
- Access functions will apply across the application — I.e. if `read` access is disabled on `email`, it will not appear in the Admin panel UI or API.
- Restricting `read` on the `email` or `username` disables the **Unlock** action in the Admin panel as this function requires access to a user-identifying field.
- When overriding the `password` field, you may need to include `hidden: true` to prevent duplicate fields being displayed in the Admin panel.
During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appended to the request for you.
### Definining Token Data
### Defining Token Data
You can specify what data gets encoded to the Cookie/JWT-Token by setting `saveToJWT` property on fields within your auth collection.
Once you have created a project, you will need to select your plan. This will determine the resources that are allocated to your project and the features that are available to you.
<Banner type="success">
Note: All Payload Cloud teams that deploy a project require a card on file.
This helps us prevent fraud and abuse on our platform. If you select a plan
with a free trial, you will not be charged until your trial period is over.
We’ll remind you 7 days before your trial ends and you can cancel anytime.
| **Region** | Select the region closest to your audience. This will ensure the fastest communication between your data and your client. |
| **Project Name** | A name for your project. You can change this at any time. |
| **Project Slug** | Choose a unique slug to identify your project. This needs to be unique for your team and you can change it any time. |
| **Team** | Select the team you want to create the project under. If this is your first project, a personal team will be created for you automatically. You can modify your team settings and invite new members at any time from the Team Settings page. |
## Build Settings
If you are deploying a new project from a template, the following settings will be automatically configured for you. If you are using your own repository, you need to make sure your build settings are accurate for your project to deploy correctly.
| **Root Directory** | The folder where your `package.json` file lives. |
| **Install Command** | The command used to install your modules, for example: `yarn install` or `npm install` |
| **Build Command** | The command used to build your application, for example: `yarn build` or `npm run build` |
| **Serve Command** | The command used to serve your application, for example: `yarn serve` or `npm run serve` |
| **Branch to Deploy** | Select the branch of your repository that you want to deploy from. This is the branch that will be used to build your project when you commit new changes. |
| **Default Domain** | Set a default domain for your project. This must be unique and you will not able to change it. You can always add a custom domain later in your project settings. |
## Environment Variables
Any of the features in Payload Cloud that require environment variables will automatically be provided to your application. If your app requires any custom environment variables, you can set them here.
<Banner type="warning">
Note: For security reasons, any variables you wish to provide to the [Admin
Panel](../admin/overview) must be prefixed with`NEXT_PUBLIC_`. Learn more
[here](../configuration/environment-vars).
</Banner>
## Payment
Payment methods can be set per project and can be updated any time. You can use team’s default payment method, or add a new one. Modify your payment methods in your Project settings / Team settings.
<Banner type="success">
**Note:** All Payload Cloud teams that deploy a project require a card on
file. This helps us prevent fraud and abuse on our platform. If you select a
plan with a free trial, you will not be charged until your trial period is
over. We’ll remind you 7 days before your trial ends and you can cancel
A deployment solution specifically designed for Node.js + MongoDB applications, offering seamless deployment of your entire stack in one place. You can get started in minutes with a one-click template or bring your own codebase with you.
Payload Cloud offers various plans tailored to meet your specific needs, including a MongoDB Atlas database, S3 file storage, and email delivery powered by [Resend](https://resend.com). To see a full breakdown of features and plans, see our [Cloud Pricing page](https://payloadcms.com/cloud-pricing).
To get started, you first need to create an account. Head over to [the login screen](https://payloadcms.com/login) and **Register for Free**.
<Banner type="success">
To create your first project, you can either select [a
template](#starting-from-a-template) or [import an existing
project](#importing-from-an-existing-codebase) from GitHub.
</Banner>
## Starting from a Template
Templates come preconfigured and provide a one-click solution to quickly deploy a new application.

_Creating a new project from a template._
After creating an account, select your desired template from the Projects page. At this point, you need to connect to authorize the Payload Cloud application with your GitHub account. Click Continue with GitHub and follow the prompts to authorize the app.
Next, select your `GitHub Scope`. If you belong to multiple organizations, they will show up here. If you do not see the organization you are looking for, you may need to adjust your GitHub app permissions.
After selecting your scope, create a unique `repository name` and select whether you want your repository to be public or private on GitHub.
<Banner type="warning">
**Note:** Public repositories can be accessed by anyone online, while private
repositories grant access only to you and anyone you explicitly authorize.
</Banner>
Once you are ready, click **Create Project**. This will clone the selected template to a new repository in your GitHub account, and take you to the configuration page to set up your project for deployment.
## Importing from an Existing Codebase
Payload Cloud works for any Node.js + MongoDB app. From the New Project page, select **import an existing Git codebase**. Choose the organization and select the repository you want to import. From here, you will be taken to the configuration page to set up your project for deployment.

_Creating a new project from an existing repository._
<Banner type="warning">
**Note:** In order to make use of the features of Payload Cloud in your own
_A screenshot of the Overview page for a Cloud project._
## Database
Your Payload Cloud project comes with a MongoDB serverless Atlas DB instance or a Dedicated Atlas cluster, depending on your plan. To interact with your cloud database, you will be provided with a MongoDB connection string. This can be found under the **Database** tab of your project.
`mongodb+srv://your_connection_string`
## File Storage
Payload Cloud gives you S3 file storage backed by Cloudflare as a CDN, and this plugin extends Payload so that all of your media will be stored in S3 rather than locally.
AWS Cognito is used for authentication to your S3 bucket. The[Payload Cloud Plugin](https://github.com/payloadcms/payload/tree/main/packages/payload-cloud)will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
### Accessing Files Outside of Payload Cloud
If you'd like to access your files outside of Payload Cloud, you'll need to retrieve some values from your project's settings and put them into your environment variables. In Payload Cloud, navigate to the File Storage tab and copy the values using the copy button. Put these values in your .env file. Also copy the Cognito Password value separately and put into your .env file as well.
When you are done, you should have the following values in your .env file:
```env
PAYLOAD_CLOUD=true
PAYLOAD_CLOUD_ENVIRONMENT=prod
PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID=
PAYLOAD_CLOUD_COGNITO_USER_POOL_ID=
PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID=
PAYLOAD_CLOUD_PROJECT_ID=
PAYLOAD_CLOUD_BUCKET=
PAYLOAD_CLOUD_BUCKET_REGION=
PAYLOAD_CLOUD_COGNITO_PASSWORD=
```
The plugin will pick up these values and use them to access your files.
## Build Settings
You can update settings from your Project’s Settings tab. Changes to your build settings will trigger a redeployment of your project.
## Environment Variables
From the Environment Variables page of the Settings tab, you can add, update and delete variables for use in your project. Like build settings, these changes will trigger a redeployment of your project.
<Banner>
Note: For security reasons, any variables you wish to provide to the [Admin
Panel](../admin/overview) must be prefixed with`NEXT_PUBLIC_`. [More
details](../configuration/environment-vars).
</Banner>
## Custom Domains
With Payload Cloud, you can add custom domain names to your project. To do so, first go to the Domains page of the Settings tab of your project. Here you can see your default domain. To add a new domain, type in the domain name you wish to use.
<Banner>
Note: do not include the protocol (http:// or https://) or any paths (/page).
Only include the domain name and extension, and optionally a subdomain. -
your-domain.com - backend.your-domain.com
</Banner>
Once you click save, a DNS record will be generated for your domain name to point to your live project. Add this record into your DNS provider’s records, and once the records are resolving properly (this can take 1hr to 48hrs in some cases), your domain will now to point to your live project.
You will also need to configure your Payload project to use your specified domain. In your `payload.config.ts` file, specify your `serverURL` with your domain:
```ts
export default buildConfig({
serverURL: 'https://example.com',
// the rest of your config,
})
```
## Email
Powered by [Resend](https://resend.com), Payload Cloud comes with integrated email support out of the box. No configuration is needed, and you can use `payload.sendEmail()` to send email right from your Payload app. To learn more about sending email with Payload, checkout the [Email Configuration](../email/overview) overview.
If you are on the Pro or Enterprise plan, you can add your own custom Email domain name. From the Email page of your project’s Settings, add the domain you wish to use for email delivery. This will generate a set of DNS records. Add these records to your DNS provider and click verify to check that your records are resolving properly. Once verified, your emails will now be sent from your custom domain name.
## Developing Locally
To make changes to your project, you will need to clone the repository defined in your project settings to your local machine. In order to run your project locally, you will need configure your local environment first. Refer to your repository’s `README.md` file to see the steps needed for your specific template.
From there, you are ready to make updates to your project. When you are ready to make your changes live, commit your changes to the branch you specified in your Project settings, and your application will automatically trigger a redeploy and build from your latest commit.
## Cloud Plugin
Projects generated from a template will come pre-configured with the official Cloud Plugin, but if you are using your own repository you will need to add this into your project. To do so, add the plugin to your Payload Config:
`pnpm add @payloadcms/payload-cloud`
```js
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
import { buildConfig } from 'payload'
export default buildConfig({
plugins: [payloadCloudPlugin()],
// rest of config
})
```
<Banner type="warning">
**Note:** If your Payload Config already has an email with transport, this
will take precedence over Payload Cloud's email service.
</Banner>
<Banner type="info">
Good to know: the Payload Cloud Plugin was previously named
`@payloadcms/plugin-cloud`. If you are using this plugin, you should update to
the new package name.
</Banner>
#### **Optional configuration**
If you wish to opt-out of any Payload cloud features, the plugin also accepts options to do so.
Within Payload Cloud, the team management feature offers you the ability to
manage your organization, team members, billing, and subscription settings.
</Banner>

_A screenshot of the Team Settings page._
## Members
Each team has members that can interact with your projects. You can invite multiple people to your team and each individual can belong to more than one team. You can assign them either `owner` or `user` permissions. Owners are able to make admin-only changes, such as deleting projects, and editing billing information.
## Adding Members
To add a new member to your team, visit your Team’s Settings page, and click “Invite Teammate”. You can then add their email address, and assign their role. Press “Save” to send the invitations, which will send an email to the invited team member where they can create a new account.
## Billing
Users can update billing settings and subscriptions for any teams where they are designated as an `owner`. To make updates to the team’s payment methods, visit the Billing page under the Team Settings tab. You can add new cards, delete cards, and set a payment method as a default. The default payment method will be used in the event that another payment method fails.
## Subscriptions
From the Subscriptions page, a team owner can see all current plans for their team. From here, you can see the price of each plan, if there is an active trial, and when you will be billed next.
## Invoices
The Invoices page will you show you the invoices for your account, as well as the status on their payment.
@@ -79,12 +79,14 @@ The following options are available:
| `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. |
| `trash` | A boolean to enable soft deletes for this collection. Defaults to `false`. [More details](../trash/overview). |
| `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). |
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
_\* An asterisk denotes that a property is required._
| `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, unless it's linked to a relationship'. |
| `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. |
| `disableCopyToLocale` | Disables the "Copy to Locale" button while editing documents within this Collection. Only applicable when localization is enabled. |
| `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. |
| `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. |
| `disableCopyToLocale` | Disables the "Copy to Locale" button while editing documents within this Collection. Only applicable when localization is enabled. |
| `groupBy` | Beta. Enable grouping by a field in the 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. |
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
| `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 in the List View. [More details](#pagination). |
| `baseFilter` | Defines a default base filter which will be applied to the List View (along with any other filters applied by the user) and internal links in Lexical Editor, |
<Banner type="warning">
**Note:** If you set `useAsTitle` to a relationship or join field, it will use
only the ID of the related document(s) as the title. To display a specific
field (i.e. title) from the related document instead, create a virtual field
that extracts the desired data, and set `useAsTitle` to that virtual field.
</Banner>
### Custom Components
@@ -176,7 +187,7 @@ The following options are available:
| `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#savebutton). |
| `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#savedraftbutton). |
| `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publishbutton). |
| `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#previewbutton). |
| `Upload` | Replace the default Upload component within the Edit View. [Upload](../upload/overview) must be enabled. [More details](../custom-components/edit-view#upload). |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](../custom-components/edit-view#beforedocumentcontrols). |
| `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document controls bar. [More details](../custom-components/edit-view#editmenuitems). |
| `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#savebutton). |
| `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#savedraftbutton). |
| `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publishbutton). |
| `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#previewbutton). |
| `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
@@ -274,7 +287,7 @@ You can also pass an object to the collection's `graphQL` property, which allows
## TypeScript
You can import types from Payload to help make writing your Collection configs easier and type-safe. There are two main types that represent the Collection Config, `CollectionConfig` and `SanitizeCollectionConfig`.
You can import types from Payload to help make writing your Collection configs easier and type-safe. There are two main types that represent the Collection Config, `CollectionConfig` and `SanitizedCollectionConfig`.
The `CollectionConfig` type represents a raw Collection Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedCollectionConfig` type represents a Collection Config after it has been fully sanitized. Generally, this is only used internally by Payload.
@@ -205,7 +205,7 @@ You can also pass an object to the global's `graphQL` property, which allows you
## TypeScript
You can import types from Payload to help make writing your Global configs easier and type-safe. There are two main types that represent the Global Config, `GlobalConfig` and `SanitizeGlobalConfig`.
You can import types from Payload to help make writing your Global configs easier and type-safe. There are two main types that represent the Global Config, `GlobalConfig` and `SanitizedGlobalConfig`.
The `GlobalConfig` type represents a raw Global Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedGlobalConfig` type represents a Global Config after it has been fully sanitized. Generally, this is only used internally by Payload.
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.
## Experimental Options
Experimental options are features that may not be fully stable and may change or be removed in future releases.
These options can be enabled in your Payload Config under the `experimental` key. You can set them like this:
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
experimental: {
localizeStatus: true,
},
})
```
The following experimental options are available related to localization:
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
## 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.
@@ -70,6 +70,7 @@ The following options are available:
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
| **`experimental`** | Configure experimental features for Payload. These may be unstable and may change or be removed in future releases. [More details](../experimental). |
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
@@ -84,6 +85,7 @@ The following options are available:
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| `folders` | An optional object to configure global folder settings. [More details](../folders/overview). |
| `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
@@ -109,7 +111,7 @@ _\* An asterisk denotes that a property is required._
Payload exposes a variety of TypeScript settings that you can leverage. These settings are used to auto-generate TypeScript interfaces for your [Collections](./collections) and [Globals](./globals), and to ensure that Payload uses your [Generated Types](../typescript/overview) for all [Local API](../local-api/overview) methods.
@@ -120,10 +122,11 @@ import { buildConfig } from 'payload'
export default buildConfig({
// ...
// highlight-start
typescript: {
// highlight-line
// ...
},
// highlight-end
})
```
@@ -212,7 +215,7 @@ For more information about what we track, take a look at our [privacy policy](/p
## Cross-origin resource sharing (CORS)#cors
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or a object with the following properties:
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or an object with the following properties:
| `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. |
| `path` \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. Must begin with a forward slash (`/`). |
| `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. |
@@ -158,7 +158,7 @@ export function MyCustomView(props: AdminViewServerProps) {
<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).
Custom View with one of the built-in [Templates](./overview#templates).
@@ -59,7 +58,7 @@ _For details on how to build Custom Views, including all available props, see [B
### 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.
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](#document-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.
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.
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.
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). |
| `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
@@ -260,6 +262,91 @@ export function MyCustomDocumentControlButton(
}
```
### editMenuItems
The `editMenuItems` property allows you to inject custom components into the 3-dot menu dropdown located in the document controls bar. This dropdown contains default options including `Create New`, `Duplicate`, `Delete`, and other options when additional features are enabled. Any custom components you add will appear below these default items.
To add `editMenuItems`, use the `components.edit.editMenuItems` property in your [Collection Config](../configuration/collections):
#### Config Example
```ts
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
components: {
edit: {
// highlight-start
editMenuItems: ['/path/to/CustomEditMenuItem'],
// highlight-end
},
},
},
}
```
Here's an example of a custom `editMenuItems` component:
#### Server Component
```tsx
import React from 'react'
import type { EditMenuItemsServerProps } from 'payload'
@@ -505,3 +505,51 @@ Payload also exports its [SCSS](https://sass-lang.com) library for reuse which i
**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>
## Performance
An often overlooked aspect of Custom Components is performance. If unchecked, Custom Components can lead to slow load times of the Admin Panel and ultimately a poor user experience.
This is different from front-end performance of your public-facing site.
<Banner type="success">
For more performance tips, see the [Performance
documentation](../performance/overview).
</Banner>
### Follow React and Next.js best practices
All Custom Components are built using [React](https://react.dev). For this reason, it is important to follow React best practices. This includes using memoization, streaming, caching, optimizing renders, using hooks appropriately, and more.
To learn more, see the [React documentation](https://react.dev/learn).
The Admin Panel itself is a [Next.js](https://nextjs.org) application. For this reason, it is _also_ important to follow Next.js best practices. This includes bundling, when to use layouts vs pages, where to place the server/client boundary, and more.
To learn more, see the [Next.js documentation](https://nextjs.org/docs).
### Reducing initial HTML size
With Server Components, be aware of what is being sent to through the server/client boundary. All props are serialized and sent through the network. This can lead to large HTML sizes and slow initial load times if too much data is being sent to the client.
To minimize this, you must be explicit about what props are sent to the client. Prefer server components and only send the necessary props to the client. This will also offset some of the JS execution to the server.
<Banner type="success">
**Tip:** Use [React Suspense](https://react.dev/reference/react/Suspense) to
progressively load components and improve perceived performance.
</Banner>
### Prevent unnecessary re-renders
If subscribing your component to form state, it may be re-rendering more often than necessary.
To do this, use the [`useFormFields`](../admin/react-hooks) hook instead of `useFields` when you only need to access specific fields.
@@ -40,7 +40,7 @@ The following options are available:
| `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.Icon` | The simplified logo used in contexts like 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). |
@@ -345,7 +345,7 @@ export default function MyCustomIcon() {
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:
To add a custom logo, use the `admin.components.graphics.Logo` property in your Payload Config:
```ts
import { buildConfig } from 'payload'
@@ -372,13 +372,13 @@ export default function MyCustomLogo() {
}
```
### Header
### header
The `Header` property allows you to inject Custom Components above the Payload 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:
To add `header` components, use the `admin.components.header` property in your Payload Config:
Database indexes are a way to optimize the performance of your database by allowing it to quickly locate and retrieve data. If you have a field that you frequently query or sort by, adding an index to that field can significantly improve the speed of those operations.
When your query runs, the database will not scan the entire document to find that one field, but will instead use the index to quickly locate the data.
To index a field, set the `index` option to `true` in your field's config:
```ts
import type { CollectionConfig } from 'payload'
export MyCollection: CollectionConfig = {
// ...
fields: [
// ...
{
name: 'title',
type: 'text',
// highlight-start
index: true,
// highlight-end
},
]
}
```
<Banner type="info">
**Note:** The `id`, `createdAt`, and `updatedAt` fields are indexed by
default.
</Banner>
<Banner type="success">
**Tip:** If you're using MongoDB, you can use [MongoDB
Compass](https://www.mongodb.com/products/compass) to visualize and manage
your indexes.
</Banner>
## Compound Indexes
In addition to indexing single fields, you can also create compound indexes that index multiple fields together. This can be useful for optimizing queries that filter or sort by multiple fields.
To create a compound index, use the `indexes` option in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
fields: [
// ...
],
indexes: [
{
fields: ['title', 'createdAt'],
unique: true, // Optional, if you want the combination of fields to be unique
},
],
}
```
## Localized fields and MongoDB indexes
When you set `index: true` or `unique: true` on a localized field, MongoDB creates one index **per locale path** (e.g., `slug.en`, `slug.da-dk`, etc.). With many locales and indexed fields, this can quickly approach MongoDB's per-collection index limit.
If you know you'll query specifically by a locale, index only those locale paths using the collection-level `indexes` option instead of setting `index: true` on the localized field. This approach gives you more control and helps avoid unnecessary indexes.
@@ -183,13 +183,13 @@ Depending on which Database Adapter you use, your migration workflow might diffe
In relational databases, migrations will be **required** for non-development database environments. But with MongoDB, you might only need to run migrations once in a while (or never even need them).
#### MongoDB
#### MongoDB#mongodb-migrations
In MongoDB, you'll only ever really need to run migrations for times where you change your database shape, and you have lots of existing data that you'd like to transform from Shape A to Shape B.
In this case, you can create a migration by running `pnpm payload migrate:create`, and then write the logic that you need to perform to migrate your documents to their new shape. You can then either run your migrations in CI before you build / deploy, or you can run them locally, against your production database, by using your production database connection string on your local computer and running the `pnpm payload migrate` command.
#### Postgres
#### Postgres#postgres-migrations
In relational databases like Postgres, migrations are a bit more important, because each time you add a new field or a new collection, you'll need to update the shape of your database to match your Payload Config (otherwise you'll see errors upon trying to read / write your data).
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
| `disableFallbackSort` | Set to `true` to disable the adapter adding a fallback sort when sorting by non-unique fields, this can affect performance in some cases but it ensures a consistent order of results. |
| `useAlternativeDropDatabase` | Set to `true` to use an alternative `dropDatabase` implementation that calls `collection.deleteMany({})` on every collection instead of sending a raw `dropDatabase` command. Payload only uses `dropDatabase` for testing purposes. Defaults to `false`. |
| `useBigIntForNumberIDs` | Set to `true` to use `BigInt` for custom ID fields of type `'number'`. Useful for databases that don't support `double` or `int32` IDs. Defaults to `false`. |
| `useJoinAggregations` | Set to `false` to disable join aggregations (which use correlated subqueries) and instead populate join fields via multiple `find` queries. Defaults to `true`. |
| `usePipelineInSortLookup` | Set to `false` to disable the use of `pipeline` in the `$lookup` aggregation in sorting. Defaults to `true`. |
## Access to Mongoose models
@@ -55,9 +60,21 @@ You can access Mongoose models as follows:
## Using other MongoDB implementations
Limitations with [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db):
You can import the `compatibilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):
- For Azure Cosmos DB you must pass `transactionOptions: false` to the adapter options. Azure Cosmos DB does not support transactions that update two and more documents in different collections, which is a common case when using Payload (via hooks).
- For Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.
- The [Join Field](../fields/join) is not supported in DocumentDB and Azure Cosmos DB, as we internally use MongoDB aggregations to query data for that field, which are limited there. This can be changed in the future.
- For DocumentDB pass `disableIndexHints: true` to disable hinting to the DB to use `id` as index which can cause problems with DocumentDB.
```ts
import { mongooseAdapter, compatibilityOptions } from '@payloadcms/db-mongodb'
export default buildConfig({
db: mongooseAdapter({
url: process.env.DATABASE_URI,
// For example, if you're using firestore:
...compatibilityOptions.firestore,
}),
})
```
We export compatibility options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:
- Azure Cosmos DB does not support transactions that update two or more documents in different collections, which is a common case when using Payload (via hooks).
- Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
export default buildConfig({
// Automatically uses proces.env.POSTGRES_URL if no options are provided.
// Automatically uses process.env.POSTGRES_URL if no options are provided.
db: vercelPostgresAdapter(),
// Optionally, can accept the same options as the @vercel/postgres package.
db: vercelPostgresAdapter({
@@ -80,6 +80,8 @@ export default buildConfig({
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
| `readReplicas` | An array of DB read replicas connection strings, can be used to offload read-heavy traffic. |
| `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks |
## Access to Drizzle
@@ -224,7 +226,7 @@ Make sure Payload doesn't overlap table names with its collections. For example,
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
To extend a table, Payload exposes `extendTable` utility to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
| `autoIncrement` | Pass `true` to enable SQLite [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary keys to ensure the same ID cannot be reused from deleted rows |
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
| `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks |
## Access to Drizzle
@@ -189,7 +190,7 @@ Make sure Payload doesn't overlap table names with its collections. For example,
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
To extend a table, Payload exposes `extendTable` utility to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
Experimental features allow you to try out new functionality before it becomes a stable part of Payload. These features may still be in active development, may have incomplete functionality, and can change or be removed in future releases without warning.
## How It Works
Experimental features are configured via the root-level `experimental` property in your [Payload Config](../configuration/overview). This property contains individual feature flags, each flag can be configured independently, allowing you to selectively opt into specific functionality.
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
This list may change without notice.
## When to Use Experimental Features
You might enable an experimental feature when:
- You want early access to new capabilities before their stable release.
- You can accept the risks of using potentially unstable functionality.
- You are testing new features in a development or staging environment.
- You wish to provide feedback to the Payload team on new functionality.
If you are working on a production application, carefully evaluate whether the benefits outweigh the risks. For most stable applications, it is recommended to wait until the feature is officially released.
<Banner type="success">
<strong>Tip:</strong> To stay up to date on experimental features or share
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as the heading in the [Admin Panel](../admin/overview) or an object with keys for each language. Auto-generated from name if not defined. |
| **`fields`** \* | Array of field types to correspond to each row of the Array. |
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More](/docs/fields/overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More details](/docs/fields/overview#validation). |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
| **`required`** | Require this field to have a value. |
| **`labels`** | Customize the row labels appearing in the Admin dashboard. |
@@ -80,7 +80,7 @@ export const MyArrayField: Field = {
}
```
The Array Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Array Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as the heading in the Admin Panel or an object with keys for each language. Auto-generated from name if not defined. |
| **`blocks`** \* | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin Panel. |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
@@ -78,7 +78,7 @@ export const MyBlocksField: Field = {
}
```
The Blocks Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Blocks Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
@@ -68,7 +68,7 @@ export const MyCodeField: Field = {
}
```
The Code Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Code Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
@@ -58,7 +58,7 @@ export const MyCollapsibleField: Field = {
}
```
The Collapsible Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Collapsible Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
@@ -65,7 +65,7 @@ export const MyDateField: Field = {
}
```
The Date Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Date Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
@@ -65,7 +65,7 @@ export const MyEmailField: Field = {
}
```
The Email Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Email Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`fields`** \* | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Defaults to the field name, if defined. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
@@ -69,7 +69,7 @@ export const MyGroupField: Field = {
}
```
The Group Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Group Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
You can also use the Group field to only visually group fields without affecting the data structure. Not defining a label will render just the grouped fields.
| **`name`** \* | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
| **`name`** \* | To be used as the property name when retrieved from the database. [More details](./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 |
| **`orderable`** | If true, enables custom ordering and joined documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
@@ -67,7 +67,7 @@ export const MyJSONField: Field = {
}
```
The JSON Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The JSON Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
@@ -38,13 +38,13 @@ export const MyNumberField: Field = {
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
@@ -70,7 +70,7 @@ export const MyNumberField: Field = {
}
```
The Number Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Number Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
@@ -100,7 +100,7 @@ Here are the available Presentational Fields:
### Virtual Fields
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the APi response through hooks, etc.
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the API response through hooks, etc.
Here are the available Virtual Fields:
@@ -157,6 +157,7 @@ The following field names are forbidden and cannot be used:
- `salt`
- `hash`
- `file`
- `status` - with Postgres Adapter and when drafts are enabled
### Field-level Hooks
@@ -303,7 +304,24 @@ The following additional properties are provided in the `ctx` object:
| `path` | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#validation-performance). |
#### Localized and Built-in Error Messages
You can return localized error messages by utilizing the translation function provided in the `req` object:
This way you can use [Custom Translations](https://payloadcms.com/docs/configuration/i18n#custom-translations) as well as Payload's built in error messages (like `validation:required` used in the example above). For a full list of available translation strings, see the [english translation file](https://github.com/payloadcms/payload/blob/main/packages/translations/src/languages/en.ts) of Payload.
#### Reusing Default Field Validations
@@ -334,7 +352,6 @@ import {
code,
date,
email,
group,
json,
number,
point,
@@ -349,11 +366,11 @@ import {
} from 'payload/shared'
```
#### Async Field Validations
#### Validation Performance
Custom validation functions can also be asynchronous depending on your needs. This makes it possible to make requests to external services or perform other miscellaneous asynchronous logic.
When writing async or computationally heavy validation functions, it is important to consider the performance implications. Within the Admin Panel, validations are executed on every change to the field, so they should be as lightweight as possible and only run when necessary.
When writing async validation functions, it is important to consider the performance implications. Validations are executed on every change to the field, so they should be as lightweight as possible. If you need to perform expensive validations, such as querying the database, consider using the `event` property in the `ctx` object to only run the validation on form submission.
If you need to perform expensive validations, such as querying the database, consider using the `event` property in the `ctx` object to only run that particular validation on form submission.
To write asynchronous validation functions, use the `async` keyword to define your function:
All [Collections](../configuration/collections) automatically generate their own ID field. If needed, you can override this behavior by providing an explicit ID field to your config. This field should either be required or have a hook to generate the ID dynamically.
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Used as a field label in the Admin Panel and to name the generated GraphQL type. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing an `label` string and a `value` string. |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
@@ -82,7 +82,7 @@ export const MyRadioField: Field = {
}
```
The Radio Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Radio Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`relationTo`** \* | Provide one or many collection `slug`s to be able to assign relationships to. |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-relationship-options). |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-relationship-options). |
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many relations instead of only one. |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with `hasMany`. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with `hasMany`. |
| **`maxDepth`** | Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/queries/depth#max-depth) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
@@ -86,14 +86,15 @@ export const MyRelationshipField: Field = {
}
```
The Relationship Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Relationship Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop (only works when `hasMany` is set to `true`). |
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More details](#sort-options) |
| **`placeholder`** | Define a custom text or function to replace the generic default placeholder |
| **`appearance`** | Set to `drawer` or `select` to change the behavior of the field. Defaults to `select`. |
### Sort Options
@@ -149,7 +150,7 @@ The `filterOptions` property can either be a `Where` query, or a function return
| `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. |
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. Will be an empty object when called on a `Filter` component within the list view. |
| `user` | An object containing the currently authenticated user. |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](./overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](./overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](./overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](./overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](./overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](./overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-options) for more details. |
@@ -54,6 +54,7 @@ export const MySelectField: Field = {
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`filterOptions`** | Dynamically filter which options are available based on the user, data, etc. [More details](#filteroptions) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/fields/relationship#linking-virtual-fields-with-relationships). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
@@ -67,6 +68,61 @@ _\* An asterisk denotes that a property is required._
used as a GraphQL enum.
</Banner>
### filterOptions
Used to dynamically filter which options are available based on the current user, document data, or other criteria.
Some examples of this might include:
- Restricting options based on a user's role, e.g. admin-only options
- Displaying different options based on the value of another field, e.g. a city/state selector
The result of `filterOptions` will determine:
- Which options are displayed in the Admin Panel
- Which options can be saved to the database
To do this, use the `filterOptions` property in your [Field Config](./overview):
**Note:** This property is similar to `filterOptions` in
[Relationship](./relationship) or [Upload](./upload) fields, except that the
return value of this function is simply an array of options, not a query
constraint.
</Banner>
## Admin Options
To customize the appearance and behavior of the Select Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
@@ -83,12 +139,13 @@ export const MySelectField: Field = {
}
```
The Select Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Select Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`isClearable`** | Set to `true` if you'd like this field to be clearable within the Admin UI. |
| **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop. (Only works when `hasMany` is set to `true`) |
| **`placeholder`** | Define a custom text or function to replace the generic default placeholder |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
@@ -70,7 +70,7 @@ export const MyTextField: Field = {
}
```
The Text Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Text Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
@@ -67,7 +67,7 @@ export const MyTextareaField: Field = {
}
```
The Textarea Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Textarea Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. **Note: the related collection must be configured to support Uploads.** |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-upload-options). |
| **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with hasMany. |
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/depth) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`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). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More details](/docs/upload/overview#collection-upload-options). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
desc: Folders allow you to group documents across collections, and are a great way to organize your content.
keywords: folders, folder, content organization
---
Folders allow you to group documents across collections, and are a great way to organize your content. Folders are built on top of relationship fields, when you enable folders on a collection, Payload adds a hidden relationship field `folders`, that relates to a folder — or no folder. Folders also have the `folder` field, allowing folders to be nested within other folders.
The configuration for folders is done in two places, the collection config and the Payload config. The collection config is where you enable folders, and the Payload config is where you configure the global folder settings.
<Banner type="warning">
**Note:** The Folders feature is currently in beta and may be subject to
change in minor versions updates prior to being stable.
</Banner>
## Folder Configuration
On the payload config, you can configure the following settings under the `folders` property:
```ts
// Type definition
type RootFoldersConfiguration = {
/**
* If true, the browse by folder view will be enabled
*
* @default true
*/
browseByFolder?: boolean
/**
* An array of functions to be ran when the folder collection is initialized
* This allows plugins to modify the collection configuration
* Ability to view hidden fields and collections related to folders
*
* @default false
*/
debug?: boolean
/**
* The Folder field name
*
* @default "folder"
*/
fieldName?: string
/**
* Slug for the folder collection
*
* @default "payload-folders"
*/
slug?: string
}
```
```ts
// Example usage
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
folders: {
// highlight-start
debug: true, // optional
collectionOverrides: [
async ({ collection }) => {
return collection
},
], // optional
fieldName: 'folder', // optional
slug: 'payload-folders', // optional
// highlight-end
},
})
```
## Collection Configuration
To enable folders on a collection, you need to set the `admin.folders` property to `true` on the collection config. This will add a hidden relationship field to the collection that relates to a folder — or no folder.
```ts
// Type definition
type CollectionFoldersConfiguration =
| boolean
| {
/**
* If true, the collection will be included in the browse by folder view
@@ -81,7 +81,7 @@ To install a Database Adapter, you can run **one** of the following commands:
#### 2. Copy Payload files into your Next.js app folder
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](<https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)>) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/%28payload%29) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground in production environments, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disableIntrospectionInProduction` | A boolean that if false will enable the GraphQL introspection in production environments, defaults to true. |
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
@@ -121,7 +121,7 @@ The following arguments are provided to the `beforeValidate` hook:
### beforeChange
Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the data that will be saved to the document is valid in accordance to your field validations. You can optionally modify the shape of data to be saved.
Immediately before validation, beforeChange hooks will run during create and update operations. At this stage, the data should be treated as unvalidated user input. There is no guarantee that required fields exist or that fields are in the correct format. As such, using this data for side effects requires manual validation. You can optionally modify the shape of the data to be saved.
```ts
import type { CollectionBeforeChangeHook } from 'payload'
@@ -160,6 +160,7 @@ The following arguments are provided to the `afterChange` hook:
@@ -23,7 +23,7 @@ Let's see examples on how context can be used in the first two scenarios mention
### Passing Data Between Hooks
To pass data between hooks, you can assign values to context in an earlier hook in the lifecycle of a request and expect it the context in a later hook.
To pass data between hooks, you can assign values to context in an earlier hook in the lifecycle of a request and expect it in the context of a later hook.
The default TypeScript interface for `context` is `{ [key: string]: unknown }`. If you prefer a more strict typing in your project or when authoring plugins for others, you can override this using the `declare` syntax.
The default TypeScript interface for `context` is `{ [key: string]: unknown }`. If you prefer a more strict typing in your project or when authoring plugins for others, you can override this using the `declare module` syntax.
This is known as "type augmentation", a TypeScript feature which allows us to add types to existing types. Simply put this in any `.ts` or `.d.ts` file:
This is known as [module augmentation / declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation), a TypeScript feature which allows us to add properties to existing types. Simply put this in any `.ts` or `.d.ts` file:
```ts
import { RequestContext as OriginalRequestContext } from 'payload'
declare module 'payload' {
// Create a new interface that merges your additional fields with the original one
// Augment the RequestContext interface to include your custom properties
export interface RequestContext {
myObject?: string
// ...
}
}
```
This will add the property `myObject` with a type of string to every context object. Make sure to follow this example correctly, as type augmentation can mess up your types if you do it wrong.
This will add the property `myObject` with a type of string to every context object. Make sure to follow this example correctly, as module augmentation can mess up your types if you do it wrong.
| **`data`** | The incoming data passed through the operation. |
| **`doc`** | The resulting Document after changes are applied. |
| **`previousDoc`** | The Document before changes were applied. |
| **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. |
@@ -93,12 +93,108 @@ All Hooks can be written as either synchronous or asynchronous functions. Choosi
#### Asynchronous
If the Hook should modify data before a Document is updated or created, and it relies on asynchronous actions such as fetching data from a third party, it might make sense to define your Hook as an asynchronous function. This way you can be sure that your Hook completes before the operation's lifecycle continues. Async hooks are run in series - so if you have two async hooks defined, the second hook will wait for the first to complete before it starts.
If the Hook should modify data before a Document is updated or created, and it relies on asynchronous actions such as fetching data from a third party, it might make sense to define your Hook as an asynchronous function. This way you can be sure that your Hook completes before the operation's lifecycle continues.
Async hooks are run in series - so if you have two async hooks defined, the second hook will wait for the first to complete before it starts.
<Banner type="success">
**Tip:** If your hook executes a long-running task that doesn't affect the
response in any way, consider [offloading it to the job
queue](#offloading-long-running-tasks). That will free up the request to
continue processing without waiting for the task to complete.
</Banner>
#### Synchronous
If your Hook simply performs a side-effect, such as updating a CRM, it might be okay to define it synchronously, so the Payload operation does not have to wait for your hook to complete.
If your Hook simply performs a side-effect, such as mutating document data, it might be okay to define it synchronously, so the Payload operation does not have to wait for your hook to complete.
## Server-only Execution
Hooks are only triggered on the server and are automatically excluded from the client-side bundle. This means that you can safely use sensitive business logic in your Hooks without worrying about exposing it to the client.
## Performance
Hooks are a powerful way to customize the behavior of your APIs, but some hooks are run very often and can add significant overhead to your requests if not optimized.
When building hooks, combine together as many of these strategies as possible to ensure your hooks are as performant as they can be.
<Banner type="success">
For more performance tips, see the [Performance
documentation](../performance/overview).
</Banner>
### Writing efficient hooks
Consider when hooks are run. One common pitfall is putting expensive logic in hooks that run very often.
For example, the `read` operation runs on every read request, so avoid putting expensive logic in a `beforeRead` or `afterRead` hook.
```ts
{
hooks: {
beforeRead: [
async () => {
// This runs on every read request - avoid expensive logic here
await doSomethingExpensive()
return data
},
],
},
}
```
Instead, you might want to use a `beforeChange` or `afterChange` hook, which only runs when a document is created or updated.
```ts
{
hooks: {
beforeChange: [
async ({ context }) => {
// This is more acceptable here, although still should be mindful of performance
await doSomethingExpensive()
// ...
},
]
},
}
```
### Using Hook Context
Use [Hook Context](./context) avoid prevent infinite loops or avoid repeating expensive operations across multiple hooks in the same request.
To learn more, see the [Hook Context documentation](./context).
### Offloading to the jobs queue
If your hooks perform any long-running tasks that don't direct affect request lifecycle, consider offloading them to the [jobs queue](../jobs-queue/overview). This will free up the request to continue processing without waiting for the task to complete.
```ts
{
hooks: {
afterChange: [
async ({ doc, req }) => {
// Offload to job queue
await req.payload.jobs.queue(...)
// ...
},
],
},
}
```
To learn more, see the [Job Queue documentation](../jobs-queue/overview).
Then in the `plugins` array of your Payload Config, call the plugin and enable any collections that require Content Source Maps.
```ts
import { buildConfig } from "payload/config"
import contentSourceMaps from "@payloadcms/plugin-csm"
import { buildConfig } from 'payload/config'
import contentSourceMaps from '@payloadcms/plugin-csm'
const config = buildConfig({
collections: [
{
slug: "pages",
slug: 'pages',
fields: [
{
name: 'slug',
type: 'text',
},
{
name: 'title,'
name: 'title',
type: 'text',
},
],
@@ -55,7 +55,7 @@ const config = buildConfig({
],
plugins: [
contentSourceMaps({
collections: ["pages"],
collections: ['pages'],
}),
],
})
@@ -63,19 +63,50 @@ const config = buildConfig({
export default config
```
Now in your Next.js app, include the `?encodeSourceMaps=true` parameter in any of your API requests. For performance reasons, this should only be done when in draft mode or on preview deployments.
## Enabling Content Source Maps
Now in your Next.js app, you need to add the `encodeSourceMaps` query parameter to your API requests. This will tell Payload to include the Content Source Maps in the API response.
<Banner type="warning">
**Note:** For performance reasons, this should only be done when in draft mode
or on preview deployments.
</Banner>
#### REST API
If you're using the REST API, include the `?encodeSourceMaps=true` search parameter.
```ts
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
If you're using the Local API, include the `encodeSourceMaps` via the `context` property.
```ts
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
const res = await payload.find({
collection: 'pages',
where: {
slug: {
equals: slug,
},
},
context: {
encodeSourceMaps: true,
},
})
}
```
And that's it! You are now ready to enter Edit Mode and begin visually editing your content.
#### Edit Mode
## Edit Mode
To see Content Link on your site, you first need to visit any preview deployment on Vercel and login using the Vercel Toolbar. When Content Source Maps are detected on the page, a pencil icon will appear in the toolbar. Clicking this icon will enable Edit Mode, highlighting all editable fields on the page in blue.
All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site. You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are automatically given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site.
You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
- Workflows are groupings of specific tasks which should be run in-order, and can be retried from a specific point of failure
- A Job is an instance of a single task or workflow which will be executed
- A Queue is a way to segment your jobs into different "groups" - for example, some to run nightly, and others to run every 10 minutes
### Visualizing Jobs in the Admin UI
By default, the internal `payload-jobs` collection is hidden from the Payload Admin Panel. To make this collection visible for debugging or inspection purposes, you can override its configuration using `jobsCollectionOverrides`.
@@ -30,7 +30,9 @@ As mentioned above, you can queue jobs, but the jobs won't run unless a worker p
### Cron jobs
You can use the `jobs.autoRun` property to configure cron jobs:
The `jobs.autoRun` property allows you to configure cron jobs that automatically run queued jobs at specified intervals. Note that this does not _queue_ new jobs - only _runs_ jobs that are already in the specified queue.
**Example**:
```ts
export default buildConfig({
@@ -49,7 +51,7 @@ export default buildConfig({
// add as many cron jobs as you want
],
shouldAutoRun: async (payload) => {
// Tell Payload if it should run jobs or not.
// Tell Payload if it should run jobs or not. This function is optional and will return true by default.
// This function will be invoked each time Payload goes to pick up and run jobs.
// If this function ever returns false, the cron schedule will be stopped.
This endpoint is automatically mounted for you and is helpful in conjunction with serverless platforms like Vercel, where you might want to use Vercel Cron to invoke a serverless function that executes your jobs.
**Query Parameters:**
- `limit`: The maximum number of jobs to run in this invocation (default: 10).
- `queue`: The name of the queue to run jobs from. If not specified, jobs will be run from the `default` queue.
- `allQueues`: If set to `true`, all jobs from all queues will be run. This will ignore the `queue` parameter.
**Vercel Cron Example**
If you're deploying on Vercel, you can add a `vercel.json` file in the root of your project that configures Vercel Cron to invoke the `run` endpoint on a cron schedule.
@@ -137,11 +145,15 @@ If you want to process jobs programmatically from your server-side code, you can
**Run all jobs:**
```ts
// Run all jobs from the `default` queue - default limit is 10
const results = await payload.jobs.run()
// You can customize the queue name and limit by passing them as arguments:
Finally, you can process jobs via the bin script that comes with Payload out of the box.
Finally, you can process jobs via the bin script that comes with Payload out of the box. By default, this script will run jobs from the `default` queue, with a limit of 10 jobs per invocation:
```sh
npx payload jobs:run --queue default --limit 10
npx payload jobs:run
```
You can override the default queue and limit by passing the `--queue` and `--limit` flags:
```sh
npx payload jobs:run --queue myQueue --limit 15
```
If you want to run all jobs from all queues, you can pass the `--all-queues` flag:
```sh
npx payload jobs:run --all-queues
```
In addition, the bin script allows you to pass a `--cron` flag to the `jobs:run` command to run the jobs on a scheduled, cron basis:
Payload's `schedule` property lets you enqueue Jobs regularly according to a cron schedule - daily, weekly, hourly, or any custom interval. This is ideal for tasks or workflows that must repeat automatically and without manual intervention.
Scheduling Jobs differs significantly from running them:
- **Queueing**: Scheduling only creates (enqueues) the Job according to your cron expression. It does not immediately execute any business logic.
- **Running**: Execution happens separately through your Jobs runner - such as autorun, or manual invocation using `payload.jobs.run()` or the `payload-jobs/run` endpoint.
Use the `schedule` property specifically when you have recurring tasks or workflows. To enqueue a single Job to run once in the future, use the `waitUntil` property instead.
## Example use cases
**Regular emails or notifications**
Send nightly digests, weekly newsletters, or hourly updates.
**Batch processing during off-hours**
Process analytics data or rebuild static sites during low-traffic times.
**Periodic data synchronization**
Regularly push or pull updates to or from external APIs.
## Handling schedules
Something needs to actually trigger the scheduling of jobs (execute the scheduling lifecycle seen below). By default, the `jobs.autorun` configuration, as well as the `/api/payload-jobs/run` will also handle scheduling for the queue specified in the `autorun` configuration.
You can disable this behavior by setting `disableScheduling: true` in your `autorun` configuration, or by passing `disableScheduling=true` to the `/api/payload-jobs/run` endpoint. This is useful if you want to handle scheduling manually, for example, by using a cron job or a serverless function that calls the `/api/payload-jobs/handle-schedules` endpoint or the `payload.jobs.handleSchedules()` local API method.
## Defining schedules on Tasks or Workflows
Schedules are defined using the `schedule` property:
This configuration only queues the Job - it does not execute it immediately. To actually run the queued Job, you configure autorun in your Payload config (note that autorun should **not** be used on serverless platforms):
```ts
export default buildConfig({
jobs: {
autoRun: [
{
cron: '* * * * *', // Runs every minute
queue: 'nightly',
},
],
tasks: [SendDigestEmail],
},
})
```
That way, Payload's scheduler will automatically enqueue the job into the `nightly` queue every day at midnight. The autorun configuration will check the `nightly` queue every minute and execute any Jobs that are due to run.
## Scheduling lifecycle
Here's how the scheduling process operates in detail:
1. **Cron evaluation**: Payload (or your external trigger in `manual` mode) identifies which schedules are due to run. To do that, it will
read the `payload-jobs-stats` global which contains information about the last time each scheduled task or workflow was run.
2. **BeforeSchedule hook**:
- The default beforeSchedule hook checks how many active or runnable jobs of the same type that have been queued by the scheduling system currently exist.
If such a job exists, it will skip scheduling a new one.
- You can provide your own `beforeSchedule` hook to customize this behavior. For example, you might want to allow multiple overlapping Jobs or dynamically set the Job input data.
3. **Enqueue Job**: Payload queues up a new job. This job will have `waitUntil` set to the next scheduled time based on the cron expression.
4. **AfterSchedule hook**:
- The default afterSchedule hook updates the `payload-jobs-stats` global metadata with the last scheduled time for the Job.
- You can provide your own afterSchedule hook to it for custom logging, metrics, or other post-scheduling actions.
## Customizing concurrency and input (Advanced)
You may want more control over concurrency or dynamically set Job inputs at scheduling time. For instance, allowing multiple overlapping Jobs to be scheduled, even if a previously scheduled job has not completed yet, or preparing dynamic data to pass to your Job handler:
```ts
import { countRunnableOrActiveJobsForQueue } from 'payload'
schedule: [
{
cron: '* * * * *', // every minute
queue: 'reports',
hooks: {
beforeSchedule: async ({ queueable, req }) => {
const runnableOrActiveJobsForQueue =
await countRunnableOrActiveJobsForQueue({
queue: queueable.scheduleConfig.queue,
req,
taskSlug: queueable.taskConfig?.slug,
workflowSlug: queueable.workflowConfig?.slug,
onlyScheduled: true,
})
// Allow up to 3 simultaneous scheduled jobs and set dynamic input
return {
shouldSchedule: runnableOrActiveJobsForQueue < 3,
input: { text: 'Hi there' },
}
},
},
},
]
```
This allows fine-grained control over how many Jobs can run simultaneously and provides dynamically computed input values each time a Job is scheduled.
## Scheduling in serverless environments
On serverless platforms, scheduling must be triggered externally since Payload does not automatically run cron schedules in ephemeral environments. You have two main ways to trigger scheduling manually:
- **Invoke via Payload's API:** `payload.jobs.handleSchedules()`
- **Use the REST API endpoint:** `/api/payload-jobs/handle-schedules`
- **Use the run endpoint, which also handles scheduling by default:** `GET /api/payload-jobs/run`
For example, on Vercel, you can set up a Vercel Cron to regularly trigger scheduling:
- **Vercel Cron Job:** Configure Vercel Cron to periodically call `GET /api/payload-jobs/handle-schedules`. If you would like to auto-run your scheduled jobs as well, you can use the `GET /api/payload-jobs/run` endpoint.
Once Jobs are queued, their execution depends entirely on your configured runner setup (e.g., autorun, or manual invocation).
@@ -33,7 +33,7 @@ Simply add a task to the `jobs.tasks` array in your Payload config. A task consi
| `onSuccess` | Function to be executed if the task succeeds. |
| `retries` | Specify the number of times that this step should be retried if it fails. If this is undefined, the task will either inherit the retries from the workflow or have no retries. If this is 0, the task will not be retried. By default, this is undefined. |
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks picks up a Job that includes this task.
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks up a Job that includes this task.
It should return an object with an `output` key, which should contain the output of the task as you've defined.
@@ -213,7 +213,7 @@ export default buildConfig({
## Nested tasks
You can run sub-tasks within an existing task, by using the `tasks` or `ìnlineTask` arguments passed to the task `handler` function:
You can run sub-tasks within an existing task, by using the `tasks` or `inlineTask` arguments passed to the task `handler` function:
| **`url`** \* | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). |
| **`url`** | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). |
| **`breakpoints`** | Array of breakpoints to be used as “device sizes” in the preview window. Each item appears as an option in the toolbar. [More details](#breakpoints). |
| **`collections`** | Array of collection slugs to enable Live Preview on. |
| **`globals`** | Array of global slugs to enable Live Preview on. |
_\* An asterisk denotes that a property is required._
### URL
The `url` property resolves to a string that points to your front-end application. This value is used as the `src` attribute of the iframe rendering your front-end. Once loaded, the Admin Panel will communicate directly with your app through `window.postMessage` events.
@@ -393,7 +393,7 @@ export default function LoginForm() {
### Logout
Logs out the current user by clearing the authentication cookie.
Logs out the current user by clearing the authentication cookie and current sessions.
#### Importing the `logout` function
@@ -401,7 +401,7 @@ Logs out the current user by clearing the authentication cookie.
import { logout } from '@payloadcms/next/auth'
```
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below.
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below. To ensure all sessions are cleared, set `allSessions: true` in the options, if you wish to logout but keep current sessions active, you can set this to `false` or leave it `undefined`.
```ts
'use server'
@@ -411,7 +411,7 @@ import config from '@payload-config'
Payload is designed with performance in mind, but its customizability means that there are many ways to configure your app that can impact performance.
With this in mind, Payload provides several options and best practices to help you optimize your app's specific performance needs. This includes the database, APIs, and Admin Panel.
Whether you're building an app or troubleshooting an existing one, follow these guidelines to ensure that it runs as quickly and efficiently as possible.
## Building your application
### Database proximity
The proximity of your database to your server can significantly impact performance. Ensure that your database is hosted in the same region as your server to minimize latency and improve response times.
### Indexing your fields
If a particular field is queried often, build an [Index](../database/indexes) for that field to produce faster queries.
When your query runs, the database will not search the entire document to find that one field, but will instead use the index to quickly locate the data.
To learn more, see the [Indexes](../database/indexes) docs.
### Querying your data
There are several ways to optimize your [Queries](../queries/overview). Many of these options directly impact overall database overhead, response sizes, and/or computational load and can significantly improve performance.
When building queries, combine as many of these options together as possible. This will ensure your queries are as efficient as they can be.
To learn more, see the [Query Performance](../queries/overview#performance) docs.
### Optimizing your APIs
When querying data through Payload APIs, the request lifecycle includes running hooks, access control, validations, and other operations that can add significant overhead to the request.
To optimize your APIs, any custom logic should be as efficient as possible. This includes writing lightweight hooks, preventing memory leaks, offloading long-running tasks, and optimizing custom validations.
To learn more, see the [Hooks Performance](../hooks/overview#performance) docs.
### Writing efficient validations
If your validation functions are asynchronous or computationally heavy, ensure they only run when necessary.
To learn more, see the [Validation Performance](../fields/overview#validation-performance) docs.
### Optimizing custom components
When building custom components in the Admin Panel, ensure that they are as efficient as possible. This includes using React best practices such as memoization, lazy loading, and avoiding unnecessary re-renders.
To learn more, see the [Custom Components Performance](../admin/custom-components#performance) docs.
## Other Best Practices
### Block references
Use [Block References](../fields/blocks#block-references) to share the same block across multiple fields without bloating the config. This will reduce the number of fields to traverse when processing permissions, etc. and can significantly reduce the amount of data sent from the server to the client in the Admin Panel.
For example, if you have a block that is used in multiple fields, you can define it once and reference it in each field.
To do this, use the `blockReferences` option in your blocks field:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
blocks: [
{
slug: 'TextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
collections: [
{
slug: 'posts',
fields: [
{
name: 'content',
type: 'blocks',
// highlight-start
blockReferences: ['TextBlock'],
blocks: [], // Required to be empty, for compatibility reasons
// highlight-end
},
],
},
{
slug: 'pages',
fields: [
{
name: 'content',
type: 'blocks',
// highlight-start
blockReferences: ['TextBlock'],
blocks: [], // Required to be empty, for compatibility reasons
// highlight-end
},
],
},
],
})
```
### Using the cached Payload instance
Ensure that you do not instantiate Payload unnecessarily. Instead, Payload provides a caching mechanism to reuse the same instance across your app.
To do this, use the `getPayload` function to get the cached instance of Payload:
```ts
import { getPayload } from 'payload'
import config from '@payload-config'
const myFunction = async () => {
const payload = await getPayload({ config })
// use payload here
}
```
### When to make direct-to-db calls
<Banner type="warning">
**Warning:** Direct database calls bypass all hooks and validations. Only use
this method when you are certain that the operation is safe and does not
require any of these features.
</Banner>
Making direct database calls can significantly improve performance by bypassing much of the request lifecycle such as hooks, validations, and other overhead associated with Payload APIs.
For example, this can be especially useful for the `update` operation, where Payload would otherwise need to make multiple API calls to fetch, update, and fetch again. Making a direct database call can reduce this to a single operation.
To do this, use the `payload.db` methods:
```ts
await payload.db.updateOne({
collection: 'posts',
id: post.id,
data: {
title: 'New Title',
},
})
```
<Banner type="warning">
**Note:** Direct database methods do not start a
[transaction](../database/transactions). You have to start that yourself.
</Banner>
#### Returning
To prevent unnecessary database computation and reduce the size of the response, you can also set `returning: false` in your direct database calls if you don't need the updated document returned to you.
```ts
await payload.db.updateOne({
collection: 'posts',
id: post.id,
data: { title: 'New Title' }, // See note above ^ about Postgres
// highlight-start
returning: false,
// highlight-end
})
```
<Banner type="warning">
**Note:** The `returning` option is only available on direct-to-db methods.
E.g. those on the `payload.db` object. It is not exposed to the Local API.
</Banner>
### Avoid bundling the entire UI library in your front-end
If your front-end imports from `@payloadcms/ui`, ensure that you do not bundle the entire package as this can significantly increase your bundle size.
To do this, import using the full path to the specific component you need:
```ts
import { Button } from '@payloadcms/ui/elements/Button'
```
Custom components within the Admin Panel, however, do not have this same restriction and can import directly from `@payloadcms/ui`:
to analyze your component tree and identify unnecessary re-renders or large
components that could be optimized.
</Banner>
## Optimizing local development
Everything mentioned above applies to local development as well, but there are a few additional steps you can take to optimize your local development experience.
### Enable Turbopack
<Banner type="warning">
**Note:** In the future this will be the default. Use at your own risk.
</Banner>
Add `--turbo` to your dev script to significantly speed up your local development server start time.
```json
{
"scripts": {
"dev": "next dev --turbo"
}
}
```
### Only bundle server packages in production
<Banner type="warning">
**Note:** This is enabled by default in `create-payload-app` since v3.28.0. If
you created your app after this version, you don't need to do anything.
</Banner>
By default, Next.js bundles both server and client code. However, during development, bundling certain server packages isn't necessary.
Payload has thousands of modules, slowing down compilation.
Setting this option skips bundling Payload server modules during development. Fewer files to compile means faster compilation speeds.
To do this, add the `devBundleServerPackages` option to `withPayload` in your `next.config.js` file:
| `defaultValue` | date | The default value of the field. |
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
### Number
Maps to a `number` input on your front-end. Used to collect a number.
@@ -421,6 +467,67 @@ formBuilderPlugin({
})
```
### Customizing the date field default value
You can custommise the default value of the date field and any other aspects of the date block in this way.
Note that the end submission source will be responsible for the timezone of the date. Payload only stores the date in UTC format.
```ts
import { fields as formFields } from '@payloadcms/plugin-form-builder'
// payload.config.ts
formBuilderPlugin({
fields: {
// date: true, // just enable it without any customizations
date: {
...formFields.date,
fields: [
...(formFields.date && 'fields' in formFields.date
? formFields.date.fields.map((field) => {
if ('name' in field && field.name === 'defaultValue') {
return {
...field,
timezone: true, // optionally enable timezone
admin: {
...field.admin,
description: 'This is a date field',
},
}
}
return field
})
: []),
],
},
},
})
```
### Preventing generated schema naming conflicts
Plugin fields can cause GraphQL type name collisions with your own blocks or collections. This results in errors like:
```plaintext
Error: Schema must contain uniquely named types but contains multiple types named "Country"
```
You can resolve this by overriding:
- `graphQL.singularName` in your collection config (for GraphQL schema conflicts)
- `interfaceName` in your block config
- `interfaceName` in the plugin field config
```ts
// payload.config.ts
formBuilderPlugin({
fields: {
country: {
interfaceName: 'CountryFormBlock', // overrides the generated type name to avoid a conflict
},
},
})
```
## Email
This plugin relies on the [email configuration](../email/overview) defined in your Payload configuration. It will read from your config and attempt to send your emails using the credentials provided.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.