### What?
Adds a new `disableGroupBy` admin config property for fields to control
their visibility in the list view GroupBy dropdown.
### Why
Previously, the GroupByBuilder was incorrectly using `disableListFilter`
to determine which fields to show in the group-by dropdown.
### How
- Added new `disableGroupBy` property to the field admin config types.
- Updated `GroupByBuilder` to filter fields based on `disableGroupBy`
instead of `disableListFilter`
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211511898438807
### What?
Fixed a typo in the ecommerce template footer component.
Changed "Crafted by Prayload" → "Crafted by Payload" in
`templates/ecommerce/src/components/Footer/index.tsx`.
### Why?
Ensures correct branding and improves professionalism of the template.
### How?
Updated the footer text string directly.
Before:
Crafted by Prayload
After:
Crafted by Payload
This PR updates the build process to generate a single
`dist/index.bundled.d.ts` file that bundles all `payload` package types.
Having one bundled declaration file makes it easy to load types into the
Monaco editor (e.g. for the new Code block), enabling full type
completion for the `payload` package.
## Example
```ts
BlocksFeature({
blocks: [
CodeBlock({
slug: 'PayloadCode',
languages: {
ts: 'TypeScript',
},
typescript: {
fetchTypes: [
{
filePath: 'file:///node_modules/payload/index.d.ts',
url: 'https://unpkg.com/payload@3.59.0-internal.e247081/dist/index.bundled.d.ts', // <= download bundled .d.ts
},
],
paths: {
payload: ['file:///node_modules/payload/index.d.ts'],
},
typeRoots: ['node_modules/@types', 'node_modules/payload'],
},
}),
],
}),
```
<img width="1506" height="866" alt="Screenshot 2025-10-01 at 12 38
54@2x"
src="https://github.com/user-attachments/assets/135b9b69-058a-42b9-afa0-daa328f64f38"
/>
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211524241290884
When you have a `hasMany: true` relationship field with at least 1 ID
that references nothing (because the actual document was deleted and
since MongoDB doesn't have foreign constraints - the relationship field
still includes that "dead" ID) graphql querying of that field fails.
This PR fixes it.
The same applies if you don't have access to some document for all DBs
### What?
This PR applies `mergeFieldStyles` to the `ArrayFieldComponent`
component, ensuring that custom admin styles such as `width` are
correctly respected when Array fields are placed inside row layouts.
### Why?
Previously, Array fields did not inherit or apply their `admin.width`
(or other merged field styles). For example, when placing two array
fields side by side inside a row with `width: '50%'`, the widths were
ignored, causing layout issues.
### How?
- Imported and used `mergeFieldStyles` within `ArrayFieldComponent`.
- Applied the merged styles to the root `<div>` via the `style` prop,
consistent with how other field components (like `TextField`) handle
styles.
Fixes#13973
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211511898438801
### What?
Updated the existing Virtual Fields section to focus on Join field types
and added a new "Virtual Field Configuration" section covering how to
make any field virtual.
### Why?
The existing virtual fields documentation was lacking depth and clarity.
### How?
- Refined existing h3 "Virtual Fields" section to focus specifically on
Join field types
- Added new h2 "Virtual Field Configuration" section with detailed
coverage of:
- Boolean virtual fields with hooks for computed values
- String path virtual fields for relationship resolution
- Virtual path syntax and requirements
- Common use cases with practical examples
Sets `reportUnusedDisableDirectives: 'error'` in our eslint config. This
will error when `// eslint-disable-*` directives are unused. It will
also auto-fix if possible.
### What?
Prevents "not found" error when trashing search-enabled documents in
localized site.
### Why?
**See issue https://github.com/payloadcms/payload/issues/13835 for
details and reproduction of bug.**
When a document is soft-deleted (has `deletedAt` timestamp), the search
plugin's `afterChange` hook tries to sync the document but fails because
`payload.findByID()` excludes trashed documents by default.
**Original buggy code** in
`packages/plugin-search/src/utilities/syncDocAsSearchIndex.ts` at lines
46-51:
```typescript
docToSyncWith = await payload.findByID({
id,
collection,
locale: syncLocale,
req,
// MISSING: trash parameter!
})
```
### How?
Added detection for trashed documents and include `trash: true`
parameter:
```typescript
// Check if document is trashed (has deletedAt field)
const isTrashDocument = doc && 'deletedAt' in doc && doc.deletedAt
docToSyncWith = await payload.findByID({
id,
collection,
locale: syncLocale,
req,
// Include trashed documents when the document being synced is trashed
trash: isTrashDocument,
})
```
### Test Coverage Added
- **Enabled trash functionality** in Posts collection for plugin-search
tests
- **Added comprehensive e2e test case** in
`test/plugin-search/int.spec.ts` that verifies:
1. Creates a published post and verifies search document creation
2. Soft deletes the post (moves to trash)
3. Verifies search document is properly synced after trash operation
4. Cleans up by permanently deleting the trashed document
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
### What?
This PR adds atomic array operations ($append and $remove) for
relationship fields with `hasMany: true` across all database adapters.
These operations allow developers to add or remove specific items from
relationship arrays without replacing the entire array.
New API:
```
// Append relationships (prevents duplicates)
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
categories: { $append: ['featured', 'trending'] }
}
})
// Remove specific relationships
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
tags: { $remove: ['draft', 'private'] }
}
})
// Works with polymorphic relationships
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
relatedItems: {
$append: [
{ relationTo: 'categories', value: 'category-id' },
{ relationTo: 'tags', value: 'tag-id' }
]
}
}
})
```
### Why?
Currently, updating relationship arrays requires replacing the entire
array which requires fetching existing data before updates. Requiring
more implementation effort and potential for errors when using the API,
in particular for bulk updates.
### How?
#### Cross-Adapter Features:
- Polymorphic relationships: Full support for relationTo:
['collection1', 'collection2']
- Localized relationships: Proper locale handling when fields are
localized
- Duplicate prevention: Ensures `$append` doesn't create duplicates
- Order preservation: Appends to end of array maintaining order
- Bulk operations: Works with `updateMany` for bulk updates
#### MongoDB Implementation:
- Converts `$append` to native `$addToSet` (prevents duplicates in
contrast to `$push`)
- Converts `$remove` to native `$pull` (targeted removal)
#### Drizzle Implementation (Postgres/SQLite):
- Uses optimized batch `INSERT` with duplicate checking for `$append`
- Uses targeted `DELETE` queries for `$remove`
- Implements timestamp-based ordering for performance
- Handles locale columns conditionally based on schema
### Limitations
The current implementation is only on database-adapter level and not
(yet) for the local API. Implementation in the localAPI will be done
separately.
This PR adds an ecommerce plugin package with both a Payload plugin and
React UI utilities for the frontend. It also adds a new ecommerce
template and new ecommerce test suite.
It also makes a change to the `cpa` package to accept a `--version` flag
to install a specific version of Payload defaulting to the latest.
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
### What
Remove references to the `.name` property in the logic for detecting
react components. Instead just rely on `typeof` and `length`.
### Why
Don't use the presence/absence of function names to detect react
components or to distinguish client vs server components.
minifiers generally do not preserve function names. e.g. in swc
[`keepFnNames` is false by
default](https://swc.rs/docs/configuration/minification#:~:text=with%20terser.-,keepFnNames,-%2C%20Defaults%20to%20false).
A recent
[PR](ba227046ef)
in Next.js optimized the representation of esm exports which meant that
many `export function Foo` declarations would loose their names. This
broke users of payloadcms since now server props were no longer
propagated to their components due to the check
[here](c2300059a6/packages/ui/src/elements/withMergedProps/index.tsx (L43)).
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
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
Fixes a bug that inserted unexpected spaces before and after link text
when converting Lexical links to HTML. The extra spaces came from
indentation in the template literal used to build the anchor markup.
### Why?
The whitespace was largely unnoticed in space-delimited languages like
English but produced visible, incorrect gaps in languages without
inter-word spacing such as Japanese.
### How?
Remove the line breaks and indentations from the link-to-HTML conversion
so the anchor markup is constructed without surrounding spaces.
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