This feat adds support for
- [D1 Cloudflare SQLite](https://developers.cloudflare.com/d1/)
- R2 storage directly (previously it was via S3 SDK)
- Cloudflare 1-click deploy template
---------
Co-authored-by: Paul Popus <paul@payloadcms.com>
### What?
Query preset WhereField component was not displaying array values
correctly. When using relationship fields with operators like "is in"
that accept multiple values, the array values were not being formatted
and displayed properly in the query preset modal.
### Why?
The original `renderCondition` function only handled single values and
date objects, but did not include logic for arrays. This caused
relationship fields with multiple selected values to either not display
correctly or throw errors when viewed in query preset modals.
### How?
- Added proper array detection with `Array.isArray()` in the
`renderCondition` function
- Created a reusable `formatValue` helper function that handles single
values, objects (dates), and arrays consistently
- For arrays, format each value and join with commas:
`operatorValue.map(formatValue).join(', ')`
- Enhanced `addListFilter` test helper to accept both single values
(`string`) and arrays (`string[]`) for relationship field testing
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211334352350024
Fixes https://github.com/payloadcms/payload/issues/13904
When using Postgres, saving a document can cause the editor to re-mount.
If the document includes a blocks field, this leads to the editor
incorrectly resetting its value to the previous state. The issue occurs
because the editor is re-mounted without recalculating the initial
Lexical state.
This re-mounting behavior is caused by how Postgres handles JSON
storage:
- With `jsonb` columns, unlike `json` columns, object key order is not
guaranteed.
- As a result, saving and reloading the Lexical editor state shifts key
order, causing `JSON.stringify()` comparisons to fail.
## Solution
To fix this, we now compare the incoming and previous editor state in a
way that ignores key order. Specifically, we switched from
`JSON.stringify()` to `dequal` for deep equality checks.
## Notes
- This code only runs when external changes occur (e.g. after saving a
document or when custom code modifies form fields).
- Because this check is infrequent, performance impact is negligible.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211488746010087
As of #13631, statically defined live preview URLs become corrupt after
the first save.
For example, if you define your URL as a string like this:
```ts
import type { CollectionConfig } from 'payload'
const MyCollection: CollectionConfig = {
// ...
admin: {
livePreview: {
url: '/hello-world'
}
}
}
```
On initial load, the iframe's src will evaluate to `.../hello-world` as
expected, but from the first save onward, the url becomes
`.../undefined`.
This is because for statically defined URLs, the `livePreviewURL`
property does not exist on the response. Despite this, we set it into
state as undefined. This is true for both collections and globals.
Initially reported on Discord here:
https://discord.com/channels/967097582721572934/967097582721572937/1421166976113442847
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211478736160152
When only a partial `labels` config is defined on a collection, the
collection defaults do not apply as expected. This leads to undefined
`singular` or `plural` properties that render as either empty or
untranslated strings on the front-end.
For example:
```ts
import type { CollectionConfig } from 'payload'
export MyCollection: CollectionConfig = {
// ...
labels: {
plural: 'Pages', // Notice that `singular` is excluded here
},
}
```
This renders empty or untranslated strings throughout the admin panel,
here are a couple examples:
<img width="326" height="211" alt="Screenshot 2025-09-26 at 10 27 40 AM"
src="https://github.com/user-attachments/assets/3872c4dd-0dac-4c1c-b417-61ddd042bbb8"
/>
<img width="330" height="267" alt="Screenshot 2025-09-26 at 10 27 51 AM"
src="https://github.com/user-attachments/assets/78772405-b5f3-45fa-9bf0-bc078f1ba976"
/>
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211478736160147
Previously, relationship fields were only filtered based on the
`payload-tenant` cookie - if the relationship points to a relation where
`doc.relation.tenant !== cookies.get('payload-tenant')`, it will fail
validation. This is good!
However, if no headers are present (e.g. when using the local API to
create or update a document), this validation will pass, even if the
document belongs to a different tenant. The following test is passing in
this PR and failing in main: `ensure relationship document with
relationship to different tenant cannot be created even if no tenant
header passed`.
This PR extends the validation logic to respect the tenant stored in the
document's data and only read the headers if the document does not have
a tenant set yet.
Old logic:
`doc.relation.tenant !== cookies.get('payload-tenant')` => fail
validation
New logic:
`doc.relation.tenant !== doc.tenant ?? cookies.get('payload-tenant')` =>
fail validation
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211456244666493
Fixes#12914.
Using the forward/back browser navigation shows stale data from the
previous visit.
For example:
1. Visit the list view, imagine a document with a title of "123"
2. Navigate to that document, update the title to "456"
3. Press the "back" button in the browser
4. Page incorrectly shows "123"
5. Press the "forward" button in the browser
6. Page incorrectly shows "123"
This is because Next.js caches those pages in memory in their
[Client-side Router
Cache](https://nextjs.org/docs/app/guides/caching#client-side-router-cache).
This enables instant loads during forward and back navigation by
restoring the previously cached response instead of making a new
request—which also happens to be our exact problem. This bfcache-like
behavior is not able to be opted out of, even if the page requires
authentication, etc.
The [hopefully temporary] fix is to force the router to make a new
request on forward/back navigation. We can do this by listening to the
popstate event and calling `router.refresh()`. This does create a flash
of stale content, however, because the refresh takes place _after_ the
cache was restored. While not wonderful, this is targeted to
specifically the forward/back events, and it's technically not
duplicative as the restored cache never made a request in the first
place.
Without native support, I'm not sure how else we'd achieve this, as
there's not way to invalidate the list view from a deeply nested
document drawer, for example.
Before:
https://github.com/user-attachments/assets/751b33b2-1926-47d2-acba-b1d47df06a6d
After:
https://github.com/user-attachments/assets/fe71938a-5c64-4756-a2c7-45dced4fcaaa
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211454879207837
Previously, clicking on a relationship with `appearance: 'drawer'`
within a lexical inline block drawer would close both drawers, instead
of opening a new drawer to select the relationship document.
Fixes https://github.com/payloadcms/payload/issues/13778
## Before
https://github.com/user-attachments/assets/371619d2-c64b-4e12-b8f3-72ad599db5a9
## After
https://github.com/user-attachments/assets/a05b9338-3b1d-4b0c-b78c-8e6b3b57014c
## Technical Notes
The issue happened due to the [`ModalContainer`'s
onClick](https://github.com/faceless-ui/modal/blob/main/src/ModalContainer/index.tsx#L43)
function being triggered when mouseUp on the relationship field is
triggered.
**Causes issue**: MouseDown => drawer opens => mouseUp
**Does not cause issue**: MouseDown => MouseUp => drawer opens
This is why the previous `setTimeout()` fix worked: it delayed the
drawer opening until the mouseUp event occured. If you click very slow,
the issue could still happen though.
I was not able to figure out _why_ the `onClick` of the ModalContainer
is triggered.
## The Fix
This is the ModalProvider `onClick` handler:
```ts
(e: MouseEvent<HTMLElement>) => {
if (closeOnBlur) closeAllModals();
if (typeof onClick === 'function') onClick(e);
},
```
The fix is to simply set `closeOnBlur` to `false`, so that
`closeAllModals` is no longer called.
I was not able to manually trigger the onClick event of the
`ModalProvider`. I figured that it is more often called by strange React
Components triggering events like maniacs (react-select) rather than
some genuine user action.
In case some piece of functionality somehow relied on this event being
triggered and then closing the modal, that piece of functionality could
manually call `closeModal()` or attach a custom `onClick` function to
the modal. That way, this mechanism will be run in a more deliberate,
expected way.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211375615406672
Fixes#10952.
The form builder plugin does not currently allow creating form
submissions from within the admin panel. This is because the fields of
the form submissions collection have `admin.readOnly` set, ultimately
disabling them during the create operation.
Instead of doing this, the user's permissions should dictate whether
these fields are read-only using access control. For example, based on
role:
```ts
import { buildConfig } from 'payload'
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
export default buildConfig({
// ...
plugins: [
formBuilderPlugin({
formSubmissionOverrides: {
access: {
update: ({ req }) => Boolean(req.user?.roles?.includes('admin')),
},
},
}),
],
})
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211454879207842
### What?
Adds a new `formatDocURL` function to collection admin configuration
that allows users to control the linkable state and URLs of first column
fields in list views.
### Why?
To provide a way to disable automatic link creation from the first
column or provide custom URLs based on document data, user permissions,
view context, and document state.
### How?
- Added `formatDocURL` function type to `CollectionAdminOptions` that
receives document data, default URL, request context, collection slug,
and view type
- Modified `renderCell` to call the function when available and handle
three return types:
- `null`: disables linking entirely
- `string`: uses custom URL
- other: falls back to no linking for safety
- Added function to server-only properties to prevent React Server
Components serialization issues
- Updated `DefaultCell` component to support custom `linkURL` prop
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211211792037945
### What?
Fixed two bugs with readonly state for server-rendered components (like
richtext fields and custom server fields):
1. Server components remained readonly after a user took over a locked
document
2. Server components were not readonly when viewing in "read only" mode
until page refresh
### Why?
Both issues stemmed from server-rendered components using their initial
readonly state that was baked in during server-side rendering, rather
than respecting dynamic readonly state changes:
1. **Takeover bug**: When a user took over a locked document,
client-side readonly state was updated but server components continued
using their initial readonly state because the server-side state wasn't
refreshed properly.
2. **Read-only view bug**: When entering "read only" mode, server
components weren't immediately updated to reflect the new readonly state
without a page refresh.
The root cause was that server-side `buildFormState` was called with
`readOnly: isLocked` during initial render, and individual field
components used this initial state rather than respecting dynamic
document-level readonly changes.
### How?
1. **Fixed race condition in `handleTakeOver`**: Made the function async
and await the `updateDocumentEditor` call before calling
`clearRouteCache()` to ensure the database is updated before page reload
2. **Improved editor comparison in `getIsLocked`**: Used `extractID()`
helper to properly compare editor IDs when the editor might be a
reference object
3. **Ensured cache clearing for all takeover scenarios**: Call
`clearRouteCache()` for both DocumentLocked modal and DocumentControls
takeovers to refresh server-side state
4. **Added Form key to force re-render**: Added `key={isLocked}` to the
Form component so it re-renders when the lock state changes, ensuring
all child components get fresh readonly state for both takeover and
read-only view scenarios
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211373627247885
### Improved tenant assignment flow
This PR improves the tenant assignment flow. I know a lot of users liked
the previous flow where the field was not injected into the document.
But the original flow, confused many of users because the tenant filter
(top left) was being used to set the tenant on the document _and_ filter
the list view.
This change shown below is aiming to solve both of those groups with a
slightly different approach. As always, feedback is welcome while we try
to really make this plugin work for everyone.
https://github.com/user-attachments/assets/ceee8b3a-c5f5-40e9-8648-f583e2412199
Added 2 new localization strings:
```
// shown in the 3 dot menu
'assign-tenant-button-label': 'Assign Tenant',
// shown when needing to assign a tenant to a NEW document
'assign-tenant-modal-title': 'Assign "{{title}}"',
```
Removed 2 localization strings:
```
'confirm-modal-tenant-switch--body',
'confirm-modal-tenant-switch--heading'
```
### What?
This PR fixes an issue where empty array fields would return `0` instead
of an empty array `[]` in form state.
The issue was caused by `rows` being initialized as `undefined` within
the array field reducer.
As a result, `rows` did not exist on array field state when initial
state was empty.
This has been updated to initialize as an empty array (`rows: []`) to
ensure consistent behavior when using `getDataByPath`.
Fixes#10712
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211439284995184
### What
Fixes a bug where collection docs paginate incorrectly when `timestamps:
false` is set — the same docs were appearing across multiple pages.
### Why
The `find` query sanitizes the `sort` parameter.
- With `timestamps: true`, it defaults to `createdAt`
- With `timestamps: false`, it falls back to `_id`
That logic is correct, but in `find.ts` we always passed `timestamps:
true`, ignoring the collection config. With the right sort applied,
pagination works as expected.
### How
`find.ts` now passes `collectionConfig.timestamps` to
`buildSortParam()`, ensuring the correct sort field is chosen.
---
Fixes#13888
### What?
- Fixes collectionSlug field not being populated, breaking export
downloads
- Works around recent UI changes that prevent custom components from
rendering for `admin.hidden` fields
- Removes `admin.hidden: true` - as the component already ensures a
hidden state by returning `null`
### Why?
- This merged [PR](https://github.com/payloadcms/payload/pull/13869)
changed how `admin.hidden` fields are rendered, causing them to bypass
custom field components entirely. The `collectionSlug` field relied on a
custom `CollectionField` component to set its value from the
ImportExportProvider context.
### How?
- Removes `admin.hidden: true`
- Field remains visually hidden with `null` return but custom component
logic now executes properly
### What?
Fixed groupBy functionality for polymorphic relationships, which was
throwing errors.
<img width="1099" height="996" alt="Screenshot 2025-09-11 at 3 10 32 PM"
src="https://github.com/user-attachments/assets/bd11d557-7f21-4e09-8fe6-6a43d777d82c"
/>
### Why?
The groupBy feature failed for polymorphic relationships because:
- `relationshipConfig` was undefined when `relationTo` is an array
(polymorphic)
- ObjectId serialization errors when passing database objects to React
client components
- hasMany relationships weren't properly flattened into individual
groups
- "No Value" groups appeared first instead of populated groups
### How?
- Handle polymorphic relationship structure `{relationTo, value}`
correctly by finding the right collection config using `relationTo`
- Add proper collection config lookup for each relation in polymorphic
relationships during populate
- Flatten hasMany relationship arrays so documents with `[Category1,
Category2]` create separate groups for each
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211331842191589
Closes#12785.
Although your live preview URL can be dynamic based on document data, it
is never recalculated after initial mount. This means if your URL is
dependent of document data that was just changed, such as a "slug"
field, the URL of the iframe does not reflect that change as expected
until the window is refreshed or you navigate back.
This also means that server-side live preview will crash when your
front-end attempts to query using a slug that no longer exists. Here's
the general flow: slug changes, autosave runs, iframe refreshes (url has
old slug), 404.
Now, we execute your live preview function on submit within form state,
and the window responds to the new URL as expected, refreshing itself
without losing its connection.
Here's the result:
https://github.com/user-attachments/assets/7dd3b147-ab6c-4103-8b2f-14d6bc889625
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211094211063140
This PR introduces support for conditionally setting allowable block
types via a new `field.filterOptions` property on the blocks field.
Closes the following feature requests:
https://github.com/payloadcms/payload/discussions/5348,
https://github.com/payloadcms/payload/discussions/4668 (partly)
## Example
```ts
fields: [
{
name: 'enabledBlocks',
type: 'text',
admin: {
description:
"Change the value of this field to change the enabled blocks of the blocksWithDynamicFilterOptions field. If it's empty, all blocks are enabled.",
},
},
{
name: 'blocksWithFilterOptions',
type: 'blocks',
filterOptions: ['block1', 'block2'],
blocks: [
{
slug: 'block1',
fields: [
{
type: 'text',
name: 'block1Text',
},
],
},
{
slug: 'block2',
fields: [
{
type: 'text',
name: 'block2Text',
},
],
},
{
slug: 'block3',
fields: [
{
type: 'text',
name: 'block3Text',
},
],
},
],
},
{
name: 'blocksWithDynamicFilterOptions',
type: 'blocks',
filterOptions: ({ siblingData: _siblingData, data }) => {
const siblingData = _siblingData as { enabledBlocks: string }
if (siblingData?.enabledBlocks !== data?.enabledBlocks) {
// Just an extra assurance that the field is working as intended
throw new Error('enabledBlocks and siblingData.enabledBlocks must be identical')
}
return siblingData?.enabledBlocks?.length ? [siblingData.enabledBlocks] : true
},
blocks: [
{
slug: 'block1',
fields: [
{
type: 'text',
name: 'block1Text',
},
],
},
{
slug: 'block2',
fields: [
{
type: 'text',
name: 'block2Text',
},
],
},
{
slug: 'block3',
fields: [
{
type: 'text',
name: 'block3Text',
},
],
},
],
},
]
```
https://github.com/user-attachments/assets/e38a804f-22fa-4fd2-a6af-ba9b0a5a04d2
# Rationale
## Why not `block.condition`?
- Individual blocks are often reused in multiple contexts, where the
logic for when they should be available may differ. It’s more
appropriate for the blocks field (typically tied to a single collection)
to determine availability.
- Hiding existing blocks when they no longer satisfy a condition would
cause issues - for example, reordering blocks would break or cause block
data to disappear. Instead, this implementation ensures consistency by
throwing a validation error if a block is no longer allowed. This aligns
with the behavior of `filterOptions` in relationship fields, rather than
`condition`.
## Why not call it `blocksFilterOptions`?
Although the type differs from relationship fields, this property is
named `filterOptions` (and not `blocksFilterOptions`) for consistency
across field types. For example, the Select field also uses
`filterOptions` despite its type being unique.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211334752795631
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