### 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
### What?
Passes the input/output encoding format explicitly when
ciphering/deciphering in the auth/crypto utility.
### Why?
To make it clearer to read and improve compatibility with other
Javascript runtimes
Co-authored-by: Ricardo Tavares <rtavares@cloudflare.com>
### 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 flaky MongoDB transaction state errors ("Attempted illegal state
transition from `[TRANSACTION_ABORTED]` to `[TRANSACTION_COMMITTED]`")
in plugin-search int tests.
### Why?
When reindexing multiple collections in parallel, individual collection
failures were calling `killTransaction()` on a shared
transaction that other parallel operations were still using, causing
MongoDB transaction state conflicts and test flakiness.
### How?
- Moved transaction cleanup to outer catch block only
- Removed individual `killTransaction` calls that created race
conditions
- Allow parallel operations to handle their own errors without aborting
the shared transaction
### 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
Fixes https://github.com/payloadcms/payload/issues/13887
ensureSafeCollectionsChange was using the `folderSlug` imported from the
constants file instead of using the `slug` passed into the
createFolderCollection function.
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
### Problem
Some Ukrainian translations contained typos and unnatural phrasing,
which could confuse users or look unpolished.
Fixed spelling mistakes and corrected mistranslations to make the text
clear and natural for Ukrainian users.
As a native Ukrainian speaker, I just went through and cleaned up any
awkward or incorrect phrases.
#### Details
- Changed `"Рахунок"` to `"Обліковий запис"` because the original term
implies a financial account, whereas this context refers to a user
account.
- Changed `"Спеціальне замовлення"` to `"Користувацьке"` because it
better conveys "custom" in the software context.
- Changed `"Редагування взято на себе"` to `"Редагування перехоплено"`
to reflect the action of taking over editing more accurately.
- Changed `"Спорожнити кошик"` to `"Очистити кошик"` for a clearer,
user-friendly wording.
- Changed `"Перейняти"` to `"Перехопити"` to better match the context of
taking control of editing.
- Changed `"Правда"`/`"Неправда"` to `"Так"`/`"Ні"` for more natural,
concise boolean representation in Ukrainian.
- Changed `"Поточний проект"` / `"Раніше був проект"` to `"Поточна
чернетка"` / `"Раніше була чернетка"` to correctly reflect the meaning
of draft in this context.
- Changed `"Локаль"` / `"Локалі"` to `"Локалізація"` / `"Локалізації"`
for consistency across all translation strings.
- Changed `"Відновлення..."` instead of previous value, which seems to
be prompt leak of original translation script (`general.restoring`)
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
### What?
Hide the "**Revert to published**" button when creating a new draft that
has never been published.
### Why?
Previously, the button was visible on new drafts, which was confusing
because there was no published version to revert to.
### How?
Updated the revert button condition to also require `hasPublishedDoc`.
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
Add Tamil translations
They hadn't been implemented yet
Added new translation files following the existing structure and
translated all relevant messages into Tamil.
<!--
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 #
-->
Signed-off-by: Anil Shebin S J <anilshebin@gmail.com>
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.
### What?
Optimize the `RelationshipProvider` to only select the `useAsTitle`
field when fetching documents via the REST API. This reduces payload
size and speeds up loading of the related document title in
the`RelationshipCell` in the table view.
### Why?
Previously, when a document had a relationship field, the full document
data was requested in the table view, even though the relationship cell
only shows the title in the UI. On large collections, this caused
unnecessary overhead and slower UI performance.
### How?
Applies a select to the REST API request made in the
`RelationshipProvider`, limiting the responses to the `useAsTitle` field
only.
### Notes
- I’m not entirely sure whether this introduces a breaking change. If it
does, could you suggest a way to make this behavior opt-in?
- For upload enabled collections, the full document must be requested,
because the relationship cell needs access to fields like `mimeType`,
`thumbailURL` etc.
- I hope we can find a way to get this merged. In the Payload projects I
work on, this change has significantly improved list view performance.
Similar to #13228
## 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/13856
When using `enableListViewSelectAPI` on upload collections the thumbnail
images require data, this PR ensures that the required data is always
selected.
### What?
This PR adds back clarification that the `secret` configured in Payload
is not used directly to sign JWT tokens, which can cause confusion when
attempting to verify tokens in other services.
### Why?
There was previously an issue (#2441) that explained this unexpected
behavior, and documentation was added to clarify it. However, that
clarification has since been removed from the current docs, which led to
confusion when I attempted to validate the JWT in another service and
received an "invalid signature" error.
### How?
- Included a brief explanation and a cautionary warning to inform
developers of this custom behavior.
Fixes#13814
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.
Fixes https://github.com/payloadcms/payload/issues/13774
EditorOptions were not being respected properly. The fix for this was to
set the following on the Editor component:
```ts
detectIndentation: false,
insertSpaces: undefined,
tabSize: undefined,
```
### Other fixes
This PR also fixed the flash when JSON fields were saved. It removed the
need for the `editorKey` which was causing the entire field to re-mount
when the json value changed. We had this work around so data could be
set externally and the height would be automatically calculated when the
editor mounted. But since the JSON value did not have a stable reference
there was no way for react to memoize it, so the key would change every
time the document was saved.
Now we pass down a `recalculatedHeightAt` which allows data to be edited
externally still, but tells the component to recalculate its height
without forcing the component to re-mount.
Installs
[@faceless-ui/modal@3.0.0](https://github.com/faceless-ui/modal/releases/tag/v3.0.0),
which now has React v19 stable listed as its peer deps. This will
prevent dependency mismatch errors when installing node modules as
`react@19.0.0-rc.0` is no longer expected.
Fixes#13818
Folder provider operations were not passing the locale query param,
causing issues when moving items into folders that had a required
localized field.
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