This PR fixes 2 issues:
- the `fieldConfig.admin.hidden` property had no effect for react server
components, because the RSC was returned before we're checking for
`admin.hidden` in RenderFields
- the `render-field` server function did not propagate fieldConfig
overrides to the clientProps. This means overriding `admin.Label` had no
effect
Adds e2e tests for both issues
This PR adds support for the following configuration:
```ts
const config = {
collections: [
{
slug: 'categories',
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'categories',
type: 'relationship',
relationTo: 'categories',
hasMany: true,
},
],
},
{
slug: 'examples',
fields: [
{
name: 'postCategoriesTitles',
type: 'text',
virtual: 'post.categories.title',
// hasMany: true - added automatically during the sanitization
},
{
type: 'relationship',
relationTo: 'posts',
name: 'post',
},
{
name: 'postsTitles',
type: 'text',
virtual: 'posts.title',
// hasMany: true - added automatically during the sanitization
},
{
type: 'relationship',
relationTo: 'posts',
name: 'posts',
hasMany: true,
},
],
},
],
}
```
In the result:
`postsTitles` - will be always populated with an array of posts titles.
`postCategoriesTitles` - will be always populated with an array of the
categories titles that are related to this post
The virtual `text` field is sanitizated to `hasMany: true`
automatically, but you can specify that manually as well.
Fixes#13705
With this PR, if the editor detects a disallowed heading in
`HeadingFeature`, it automatically converts it to the lowest allowed
heading.
I've also verified that disallowed headings aren't introduced when
pasting from the clipboard if the HeadingFeature isn't registered at
all. The reason this works is because the LexicalEditor doesn't have the
HeadingNode in that case.
## Why this exists
Lexical in Payload is a React Server Component (RSC). Historically that
created three headaches:
1. You couldn’t render the editor directly from the client.
2. Features like blocks, tables, upload and link drawers require the
server to know the shape of nested sub‑fields at render time. If you
tried to render on demand, the server didn’t know those schemas.
3. The rich text field is designed to live inside a Form. For simple use
cases, setting up a full form just to manage editor state was
cumbersome.
## What’s new
We now ship a client component, `<RenderLexical />`, that renders a
Lexical editor **on demand** while still covering the full feature set.
On mount, it calls a server action to render the editor on the server
using the new `render-field` server action. That server render gives
Lexical everything it needs (including nested field schemas) and returns
a ready‑to‑hydrate editor.
## Example - Rendering in custom component within existing Form
```tsx
'use client'
import type { JSONFieldClientComponent } from 'payload'
import { buildEditorState, RenderLexical } from '@payloadcms/richtext-lexical/client'
import { lexicalFullyFeaturedSlug } from '../../slugs.js'
export const Component: JSONFieldClientComponent = (args) => {
return (
<div>
Fully-Featured Component:
<RenderLexical
field={{ name: 'json' }}
initialValue={buildEditorState({ text: 'defaultValue' })}
schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`}
/>
</div>
)
}
```
## Example - Rendering outside of Form, manually managing richText
values
```ts
'use client'
import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
import type { JSONFieldClientComponent } from 'payload'
import { buildEditorState, RenderLexical } from '@payloadcms/richtext-lexical/client'
import React, { useState } from 'react'
import { lexicalFullyFeaturedSlug } from '../../slugs.js'
export const Component: JSONFieldClientComponent = (args) => {
const [value, setValue] = useState<DefaultTypedEditorState | undefined>(() =>
buildEditorState({ text: 'state default' }),
)
const handleReset = React.useCallback(() => {
setValue(buildEditorState({ text: 'state default' }))
}, [])
return (
<div>
Default Component:
<RenderLexical
field={{ name: 'json' }}
initialValue={buildEditorState({ text: 'defaultValue' })}
schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`}
setValue={setValue as any}
value={value}
/>
<button onClick={handleReset} style={{ marginTop: 8 }} type="button">
Reset Editor State
</button>
</div>
)
}
```
## How it works (under the hood)
- On first render, `<RenderLexical />` calls the server function
`render-field` (wired into @payloadcms/next), passing a schemaPath.
- The server loads the exact field config and its client schema map for
that path, renders the Lexical editor server‑side (so nested features
like blocks/tables/relationships are fully known), and returns the
component tree.
- While waiting, the client shows a small shimmer skeleton.
- Inside Forms, RenderLexical plugs into the parent form via useField;
outside Forms, you can fully control the value by passing
value/setValue.
## Type Improvements
While implementing the `buildEditorState` helper function for our test
suite, I noticed some issues with our `TypedEditorState` type:
- nodes were no longer narrowed by their node.type types
- upon fixing this issue, the type was no longer compatible with the
generated types. To address this, I had to weaken the generated type a
bit.
In order to ensure the type will keep functioning as intended from now
on, this PR also adds some type tests
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211110462564644
Fixes https://github.com/payloadcms/payload/issues/13833
When generating graphql schemas, named tabs were not properly being
accounted for. This PR fixes that and ensure that the correct schema
types are generated for named tabs.
Before, unnamed, unlabelled group were part of an unlabeled collapsible
in the version diff view. This means, all it displayed was a clickable,
empty rectangle. This looks like a bug rather than intended.
This PR displays a new <Unnamed Group> label in these cases.
It also fixes the incorrect `NamedGroupFieldClient` type. Previously,
that type did not include the `name` property, even though it was
available on the client.
Before:
<img width="2372" height="688" alt="Screenshot 2025-09-16 at 18 57
45@2x"
src="https://github.com/user-attachments/assets/0f351f84-a00f-4067-aa40-d0e8fbfd5f0b"
/>
After:
<img width="2326" height="598" alt="Screenshot 2025-09-16 at 18 56
14@2x"
src="https://github.com/user-attachments/assets/bddee841-8218-4a90-a052-9875c5f252c0"
/>
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211375615406676
Assuming you have 3 block/array rows and you only modify the middle one
- the version view would still display row 1 and row 3:
<img width="2354" height="1224" alt="Screenshot 2025-09-16 at 16 15
22@2x"
src="https://github.com/user-attachments/assets/5f823276-fda2-4192-a7d3-482f2a2228f9"
/>
After this PR, it's now displayed correctly:
<img width="2368" height="980" alt="Screenshot 2025-09-16 at 16 15
09@2x"
src="https://github.com/user-attachments/assets/7fc5ee25-f925-4c41-b62a-9b33652e19f9"
/>
## The Fix
The generated version fields will contain holes in the `rows` array for
rows that have no changes. The fix is to simply skip rendering those
rows. We still need to keep those holes in order to maintain the correct
row indexes.
Additionally, this PR improves the naming of some legacy variables and
function arguments that I missed out on during the version view
overhaul:
> comparison => valueFrom
> version => valueTo
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211267837905382
Fixes an issue with the new experimental `enableListViewSelectAPI`
config option.
Group fields were not populating properly in the list view
### Before (incorrect)
```ts
{
group.field: true
}
```
### After (correct)
```ts
{
group: {
field: true
}
}
```
* The `pagination` property was missing in `findVersions` and
`findGlobalVersions` Local API operations, although the actual functions
did have it -
1b93c4becc/packages/payload/src/collections/operations/findVersions.ts (L25)
* The handling of the `pagination` property in those functions was
broken, this PR fixes it.
### What?
Skip field validation when trashing documents with empty required
fields.
### Why?
When trashing a document that was saved as a draft with empty required
fields, Payload would run full validation and fail with "The following
fields are invalid" errors. This happened because trash operations were
treated as regular updates that require full field validation, even
though trashing is just a metadata change (setting `deletedAt`) and
shouldn't be blocked by content validation issues.
### How?
- Modified `skipValidation` logic in `updateDocument()` to skip
validation when `deletedAt` is being set in the update data
Fixes#13706
Follow-up to #12893.
### What?
Removes cases where submitting a document as a draft in a collection
with versioning enabled would cause publishing validation errors to be
displayed on further document form changes. An example case is when
saving the draft failed due to a `beforeChange` hook throwing an
`APIError`.
### How
The behavior change is that the form state is marked as un-submitted
post a submit failure as a draft. The form not being considered as
submitted results in `packages/ui/src/views/Edit/index.tsx` to use
`skipValidation: true`.
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
### What?
Remove `select` parameter from database operations in update operation
during version creation to fix malformed version data.
### Why?
The `select` parameter was being passed to `saveVersion()` in update
operations, causing versions to only contain selected fields
### How?
- Removed `select` parameter from `payload.db.updateOne()` calls in
update operations
- Removed `select` parameter from `saveVersion()` call in update
operation
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211334352350013
Fixes#13756
The findByID endpoint, by default, expects the data for localized fields
to be an object, values mapped to the locale. This is not the case for
client-side live preview, as we send already-flattened data to the
findByID endpoint.
For localized fields where the value is an object (richText/json/group),
the afterRead hook handler would attempt to flatten the field value,
even though it was already flattened.
## Solution
The solution is to expose a `flattenLocales` arg to the findByID
endpoint (default: true) and pass `flattenLocales: false` from the
client-side live preview request handler.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211334752795627
Similar spirit as #13714.
Permissions are embedded into the page response, exposing some field
names to unauthenticated users.
For example, when setting `read: () => false` on a field, that field's
name is now included in the response due to its presence in the
permissions object.
We now search the HTML source directly in the test, similar to "view
source" in the browser, which will be much effective at preventing
regression going forward.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211347942663256
Follow-up to #13714.
Fully sanitizes the unauthenticated client config to exclude much of the
users collection, including fields, etc. These are not required of the
login flow and are now completely omitted along with other unnecessary
properties.
This is closely aligned with the goals of the original PR, and as an
added bonus, makes the config _even smaller_ than it already was for
unauthenticated users.
Needs #13790.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211332845301588
### What?
Fix two live preview issues affecting client-side navigation:
1. Stale preview data leaking between pages using `useLivePreview`.
2. Erroneous fetches to `/api/undefined` and incorrect content rendering
when preview events lack slugs.
### Why?
The live-preview module cached merged preview data globally, which
persisted across route changes, causing a new page to render with the
previous page’s data.
The client attempted to merge when preview events didn’t specify
collectionSlug or globalSlug, producing an endpoint of undefined and
triggering requests to /api/undefined, sometimes overwriting state with
mismatched content.
### How?
Clear the internal cache at the time of `subscribe()` so each page using
`useLivePreview` starts from a clean slate.
In `handleMessage`, only call `mergeData` when `collectionSlug` or
`globalSlug` is present; otherwise return `initialData` and perform no
request.
Fixes#13792
Redirecting from login to any non-auth collection crashes the page with
the following error:
```
Cannot read properties of null (reading 'fields')
```
TL;DR: the page-level config context was threading stale methods from
the root config provider.
#### Background
The client config is now gated behind authentication as of #13714, so it
changes based on authentication status. If the root layout is mounted
before authentication, it puts the unauthenticated client config into
state for the entire app to consume.
On login, the root layout does not re-render, so the page itself needs
to generate a fresh client config and sync it up.
This leads to race conditions, however, where if the login page included
a `?redirect=` param, the redirect would take place _before_ the
page-level client config could sync to the layout, and ultimately crash
the page. This was addressed in #13786.
While this fixed redirects to the "users" collection, this collection is
_already_ included in the client config (soon to be omitted by #13785).
So if you redirect to any other collection, the above error occurs.
#### Problem
The page-level config context is only overriding the `config` property,
keeping stale methods from the root config provider. This means calling
`getEntityConfig` during this moment in the time would reference the
stale config, although `config` itself would be fresh.
#### Solution
Wrap the page with an entirely new context provider. Do not thread
inherited methods from the root provider, this way all new methods get
instantiated using the fresh config.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211332845301596
### What?
Opening a document with an expired lock and richtext caused the richtext
to be read-only.
### Why?
The changes in #13579 made the richtext read only if isLocked is set.
But the server side implementation of isLocked did not consider expired
locks.
### How?
Update the server-side getIsLocked to also consider expired locks by not
loading them.
## Problem
When logging in with a `?redirect=`, the target page may crash. This
happens because the update from `SyncClientConfig` is applied in a
`useEffect`, which runs **_after_** the target page's client components
render. As a result, those components read the unauthenticated client
config on first render - even though they expect the authenticated one.
## Steps To Reproduce
1. logout
2. login with ?redirect=
3. document client component incorrectly still receives unauthenticated
client config on render
4. THEN the SyncClientConfig useEffect runs. Too late
5. Potential error (depending on the page, e.g. document view) -
document client component expects sth to be there, but it is not
## Solution
This PR replaces `SyncClientConfig` with a `RootPageConfigProvider`.
This new provider shadows the root layout’s `ConfigProvider` and ensures
the correct client config is available **_immediately_**.
It still updates the config using `useEffect` (the same
way`SyncClientConfig` did), but with one key difference:
- During the brief window between the redirect and the effect running,
it overrides the root layout’s config and provides the fresh,
authenticated config from the root page via the `RootConfigContext`.
This guarantees that client components on the target page receive the
correct config on first render, preventing errors caused by reading the
outdated unauthenticated config.
## Additional change - get rid of `UnsanitizedClientConfig` and
`sanitizeClientConfig`
Those functions added unnecessary complexity, just to build the
blocksMap. I removed those and perform the building of the `blocksMap`
server-side - directly in `createClientConfig`.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211334752795621
Follow up to #11375.
When setting `filterOptions` on relationship or upload fields _that are
nested within a named field_, those options won't be applied to the
`Filter` component in the list view.
This is because of how we key the results when resolving `filterOptions`
on the server. Instead of using the field path as expected, we were
using the field name, causing a failed lookup on the front-end. This
also solves an issue where two fields with the same name would override
each other's `filterOptions`, since field names alone are not unique.
Unrelated: this PR also does some general housekeeping to e2e test
helpers.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211332845301583
When sending REST API requests with multipart/form-data, e.g. PATCH or
POST within the admin panel, a request body larger than 1MB throws the
following error:
```
Unterminated string in JSON at position...
```
This is because there are sensible defaults imposed by the HTML form
data parser (currently using
[busboy](https://github.com/fastify/busboy)). If your documents exceed
this limit, you may run into this error when editing them within the
admin panel.
To support large documents over 1MB, use the new `bodyParser` property
on the root config:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
bodyParser: {
limits: {
fieldSize: 2 * 1024 * 1024, // This will allow requests containing up to 2MB of multipart/form-data
}
}
}
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211317005907885
Adds support for the `beforeOperation` hook on globals. Runs before all
other hooks to either modify the arguments that operations receive, or
perform side-effects before an operation begins.
```ts
import type { GlobalConfig } from 'payload'
const MyGlobal: GlobalConfig = {
// ...
hooks: {
beforeOperation: []
}
}
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211317005907890
### What?
Added support for `disableListColumn` and `disableListFilter` admin
properties on imageSize configurations that automatically apply to all
fields within the corresponding size group.
### Why?
Upload collections with multiple image sizes can clutter the admin list
view with many size-specific columns and filters. This feature allows
developers to selectively hide size fields from list views while keeping
them accessible in the document edit view.
### How?
Modified `getBaseFields.ts` to inherit admin properties from imageSize
configuration and apply them to all nested fields (url, width, height,
mimeType, filesize, filename) within each size group. The implementation
uses conditional spread operators to only apply these properties when
explicitly set to `true`, maintaining backward compatibility.
Saves folder preferences when navigating from list to folder view. This
is a UX improvement so users don't need to click by-folder every time
they go back to the list view.
Adds a new property to the config that enables the Select API in the
list view. This is a performance opt-in, where only the active columns
(and those specified in `forceSelect`) are queried. This can greatly
improve performance, especially for collections with large documents or
many fields.
To enable this, use the `admin.enableListViewSelectAPI` in your
Collection Config:
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
// ...
admin: {
enableListViewSelectAPI: true // This will select only the active columns (and any `forceSelect` fields)
}
}
```
Note: The `enableListViewSelectAPI` property is currently labeled as
experimental, as it will likely become the default behavior in v4 and be
deprecated. The reason it cannot be the default now is because cells or
other components may be relying on fully populated data, which will no
longer be the case when using `select`.
For example, if your component relies on a "title" field, this field
will _**not**_ exist if the column is **_inactive_**:
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
// ...
fields: [
// ...
{
name: 'myField',
type: 'text',
hooks: {
afterRead: [
({ doc }) => doc.title // `title` will only be populated if the column is active
]
}
}
]
}
```
There are other cases that might be affected by this change as well, for
example any components relying on the `data` object returned by the
`useListQuery()` hook:
```ts
'use client'
export const MyComponent = () => {
const { data } = useListQuery() // `data.docs` will only contain fields that are selected
// ...
}
```
To ensure title is always present, you will need to add that field to
the `forceSelect` property in your Collection Config:
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
// ...
forceSelect: {
title: true
}
}
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211248751470559
Deflakes the uploads e2e suite. The CORS test was not waiting not long
enough for the response and ultimately fails.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211295974585601
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
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?
- 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?
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?
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>
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
}
```
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
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