This PR significantly improves performance when navigating through the admin panel by reducing the number of times `initReq` is called. Previously, `initReq`—which handles expensive tasks like initializing Payload and running access control—was called **three times** for a single page load (for the root layout, the root page, and the notFound page).
We initially tried to use React Cache to ensure `initReq` only ran once per request. However, because React Cache performs a shallow object reference check on function arguments, the configuration object we passed (`configPromise`) and the `overrides` object never maintained the same reference, causing the cache to miss.
### What’s Changed
* **New `getInitReqContainer` Helper**
We introduced a helper that provides a stable object reference throughout the entire request. This allows React to properly cache the output, ensuring `initReq` doesn’t get triggered multiple times by mistake.
* **Splitting `initReq` into Two Functions**
The `initReq` logic was split into:
* **`initPartialReq`:** Runs only **once** per request, handling tasks that do not depend on page-level data (e.g., calling `.auth`, which performs a DB request).
* **`initReq`:** Runs **twice** (once for Layout+NotFound page and once for main page), handling tasks, most notably access control, that rely on page-level data such as locale or query parameters. The NotFound page will share the same req as the layout page, as it's not localized, and its access control wouldn't need to access page query / url / locale, just like the layout.
* **Remove duplicative logic**
* Previously, a lot of logic was run in **both** `initReq` **and** the respective page / layout. This was completely unnecessary, as `initReq` was already running that logic. This PR returns the calculated variables from `initReq`, so they don't have to be duplicatively calculated again.
### Performance Gains
* Previously:
* `.auth` call ran **3 times**
* Access control ran **3 times**
* Now:
* `.auth` call runs **1 time**
* Access control runs **2 times**
This change yields a noticeable performance improvement by cutting down on redundant work.
Fixes https://github.com/payloadcms/payload/issues/9767
We allow failing a job queue task by returning `{ state: 'failed' }` from the task, instead of throwing an error. However, previously, this threw an error when trying to update the task in the database. Additionally, it was not possible to customize the error message.
This PR fixes that by letting you return `errorMessage` alongside `{ state: 'failed' }`, and by ensuring the error is transformed into proper json before saving it to the `error` column.
Ensures that even if you pass `enabled: false` to the storage adapter
options, e.g:
```ts
s3Storage({
enabled: false,
collections: {
[mediaSlug]: true,
},
bucket: process.env.S3_BUCKET,
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
},
})
```
the client handler component is added to the import map. This prevents
errors when you use the adapter only on production, but you don't
regenerate the import map before running the build
This PR updates the reproduction guide to reference
`create-payload-app@latest -t blank` instead of `@beta`, ensuring users
follow the latest stable release when setting up a minimal reproduction.
Maintains column state in the URL. This makes it possible to share
direct links to the list view in a specific column order or active
column state, similar to the behavior of filters. This also makes it
possible to change both the filters and columns in the same rendering
cycle, a requirement of the "list presets" feature being worked on here:
#11330.
For example:
```
?columns=%5B"title"%2C"content"%2C"-updatedAt"%2C"createdAt"%2C"id"%5D
```
The `-` prefix denotes that the column is inactive.
This strategy performs a single round trip to the server, ultimately
simplifying the table columns provider as it no longer needs to request
a newly rendered table for itself. Without this change, column state
would need to be replaced first, followed by a change to the filters.
This would make an unnecessary number of requests to the server and
briefly render the UI in a stale state.
This all happens behind an optimistic update, where the state of the
columns is immediately reflected in the UI while the request takes place
in the background.
Technically speaking, an additional database query in performed compared
to the old strategy, whereas before we'd send the data through the
request to avoid this. But this is a necessary tradeoff and doesn't have
huge performance implications. One could argue that this is actually a
good thing, as the data might have changed in the background which would
not have been reflected in the result otherwise.
### What?
This PR reverts a presumably accidental change made in
[b80010b1a1](b80010b1a1),
that broke the email verification feature in v3.24.0 and onwards.
### Why?
Through the missing verify in `const [collectionSlug, verify, token] =
params.segments`, the token value was always the string `verify`
This PR adds a new `returning` option to various db adapter methods. Setting it to `false` where the return value is not used will lead to performance gains, as we don't have to do additional db calls to fetch the updated document and then sanitize it.
### What?
`value` within the beforeValidate field hook was not correctly falling
back to the document value when no value was passed inside the request
for the field.
### Why?
The fallback logic was running after the beforeValidate field hooks are
called.
### How?
Run the fallback logic before running the beforeValidate field hooks.
Fixes https://github.com/payloadcms/payload/issues/10923
When visiting a collection's list view, the nav item corresponding to
that collection correctly appears in an active state, but is still
rendered as an anchor tag. This makes it possible to reload the current
page by simply clicking the link, which is a problem because this
performs an unnecessary server roundtrip. This is especially apparent
when search params exist in the current URL, as the href on the link
does not.
Unrelated: also cleans up leftover code that was missed in this PR:
#11155
This PR resolves an issue where the `href` for the Logout button in the
admin panel included duplicate `basePath` values when `basePath` was set
in `next.config.js`.
The Logout button was recently updated to use `NextLink` (`next/link`),
which automatically applies the `basePath` from the Next.js
configuration. As a result, manually adding the `basePath` to the `href`
is no longer necessary.
Relevant PRs that modified this behavior originally:
- #9275
- #11155
### What?
The idea of this plugin is to only add constraints when a user is
present on a request. This change makes it so access control only
applies to admin panel users as they are the ones assigned to tenants.
This change allows you to more freely write access functions on tenant
enabled collections. Say you have 2 auth enabled collections, the plugin
would incorrectly assume since there is a user on the req that it needs
to apply tenant constraints. When really, you should be able to just add
in your own access check for `req.user.collection` and return true/false
if you want to prevent/allow other auth enabled collections for certain
operations.
```ts
import { Access } from 'payload'
const readByTenant: Access = ({ req }) => {
const { user } = req
if (!user || user.collection === 'auth2') return false
return true
}
```
When you have a function like this that returns `true` and the
collection is multi-tenant enabled - the plugin injects constraints
ensuring the user on the request is assigned to the tenant on the doc
being accessed.
Before this change, you would need to opt out of access control with
`useTenantAccess` and then wire up your own access function:
```ts
import type { Access } from 'payload'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'
export const tenantAccess: Access = async ({ req: { user } }) => {
if (user) {
if (user.collection === 'auth2') {
return true
}
// Before, you would need to re-implement
// internal multi-tenant access constraints
if (user.roles?.includes('super-admin')) return true
return getTenantAccess({
fieldName: 'tenant',
user,
})
}
return false
}
```
After this change you would not need to opt out of `useTenantAccess` and
can just write:
```ts
import type { Access } from 'payload'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'
export const tenantAccess: Access = async ({ req: { user } }) => {
return Boolean(user)
}
```
This is because internally the plugin will only add the tenant
constraint when the access function returns true/Where _AND_ the user
belongs to the admin panel users collection.
🤖 Automated bump of templates for v3.25.0
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Fixes https://github.com/payloadcms/payload/issues/6648
This PR introduces a new `useFormBackgroundProcessing` hook and a corresponding `setBackgroundProcessing` function in the `useForm` hook.
Unlike `useFormProcessing` / `setProcessing`, which mark the entire form as read-only, this new approach only disables the Publish button during autosaving, keeping form fields editable for a better user experience.
I named it `backgroundProcessing` because it should run behind the scenes without disrupting the user. You could argue that it is a bit more generic than something like `isAutosaving`, but it signals intent: Background = do not disrupt the user.
### What?
For the join field query adds ability to specify `count: true`, example:
```ts
const result = await payload.find({
joins: {
'group.relatedPosts': {
sort: '-title',
count: true,
},
},
collection: "categories",
})
result.group?.relatedPosts?.totalDocs // available
```
### Why?
Can be useful to implement full pagination / show total related
documents count in the UI.
### How?
Implements the logic in database adapters. In MongoDB it's additional
`$lookup` that has `$count` in the pipeline. In SQL, it's additional
subquery with `COUNT(*)`. Preserves the current behavior by default,
since counting introduces overhead.
Additionally, fixes a typescript generation error for join fields.
Before, `docs` and `hasNextPage` were marked as nullable, which is not
true, these fields cannot be `null`.
Additionally, fixes threading of `joinQuery` in
`transform/read/traverseFields` for group / tab fields recursive calls.
Added a new Reserved Field Names section to the migration guide.
Clarified that certain field names (`__v`, `salt`, `hash`, `file`, etc.)
are reserved for internal use and will be sanitized from the config if
used.
Included additional reserved names specific to `MongoDB`, `auth`-enabled
collections, and `upload`-enabled collections.
Added a note recommending against using field names with an underscore
(`_`) prefix, as they are reserved for internal columns and may cause
conflicts in `SQL` and other contexts.
Fixes#11159
This reverts commit 69c0d09 in #11390.
In order to future proof column prefs, it probably is best to continue
to use the current shape. This change was intended to ensure that as
little transformation to URL params was made as possible for #11387, but
we will likely transform them after all.
This will ensure that we can add support for additional properties over
time, as needed. For example, if we hypothetically wanted to add a
custom `label` or similar feature to columns prefs, it would make more
sense to use explicit properties to identity `accessor` and `active`.
For example:
```ts
[
{
accessor: "title",
active: true,
label: 'Custom Label' // hypothetical
}
]
```
This bumps next.js to 15.2.0 in our monorepo, as well as all @types/react and @types/react-dom versions. Additionally, it removes the obsolete `peerDependencies` property from our root package.json.
This PR also fixes 2 bugs introduced by Next.js 15.2.0. This highlights why running our test suite against the latest Next.js, to make sure Payload is compatible, version is important.
## 1. handleWhereChange running endlessly
Upgrading to Next.js 15.2.0 caused `handleWhereChange` to be continuously called by a `useEffect` when the list view filters were opened, leading to a React error - I did not investigate why upgrading the Next.js version caused that, but this PR fixes it by making use of the more predictable `useEffectEvent`.
## 2. Custom Block and Array label React key errors
Upgrading to Next.js 15.2.0 caused react key errors when rendering custom block and array row labels on the server. This has been fixed by rendering those with a key
## 3. Table React key errors
When rendering a `Table`, a React key error is thrown since Next.js 15.2.0
This PR improves existing JSX converter docs and adds 2 new sections:
- **converting internal links** - addresses why a `"found internal link, but internalDocToHref is not provided"` error is thrown, and how to get around it
- **Overriding default JSX Converters**
Lexical server features are able to add components to the import map through the `componentImports` property. As of now, the client feature did not have access to those. This is usually not necessary, as those import map entries are used internally to render custom components server-side, e.g. when a request to the form state endpoint is made.
However, in some cases, these import map entries need to be accessed by the client feature (see "Why" section below).
This PR ensures that keyed `componentImports` entries are made available to the client feature via the new `featureClientImportMap` property.
## Why?
This is a prerequisite of the lexical [wrapper blocks PR](https://github.com/payloadcms/payload/pull/9289), where wrapper block custom components need to be made to the ClientFeature. The ClientFeature is where the wrapper block node is registered - in order to generate the wrapper block node, we need access to the component
Removes all unnecessary `page.waitForURL` methods within e2e tests.
These are unneeded when following a `page.goto` call because the
subsequent page load is already being awaited.
It is only a requirement when:
- Clicking a link and expecting navigation
- Expecting a redirect after a route change
- Waiting for a change in search params
Previously, behavior with custom IDs and `select` query was incorrect.
By default, the `id` field is guaranteed to be selected, even if it
doesn't exist in the `select` query, this wasn't true for custom IDs.
Previously, `updateOne` was using `buildFindManyArgs` and `findFirst` just to retrieve the ID of the document to update, which is a huge function that's not necessary to run just to get the document ID.
This PR refactors it to use a simple `db.select` query to retrieve the ID
This PR fixes an issue where bulk upload attempts to generate thumbnails
for non-image files, causing errors on the page.
The fix ensures that thumbnail generation is skipped for non-image
files, preventing unnecessary errors.
Fixes#10428
Previously, `hasNextPage` was working incorrectly with polymorphic joins
(that have an array of `collection`) in MongoDB.
This PR fixes it and adds extra assertions to the polymorphic joins
test.
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Transforms how column prefs are stored in the database. This change
reduces the complexity of the `columns` property by removing the
unnecessary `accessor` and `active` keys.
This change is necessary in order to [maintain column state in the
URL](https://github.com/payloadcms/payload/pull/11387), where the state
itself needs to be as concise as possible. Does so in a non-breaking
way, where the old column shape is transformed as needed.
Here's an example:
Before:
```ts
[
{
accessor: "title",
active: true
}
]
```
After:
```ts
[
{
title: true
}
]
```
Since codeowner approvals are not currently required, the codeowners
file is only serving to add reviewers to PRs.
Removing the codeowners file for now as this is not desired. Can be
re-introduced at a later date if required approvers are implemented.
### What?
Unable to update json fields externally. For example, calling `setValue`
on a json field would not be reflected in the admin panel UI.
### Why?
JSON fields use the monaco editor to manage state internally, so
programmatically updating the value in state does not change the
internal value.
### How?
Set a ref when the user updates the value and then unset the ref after
the change is complete.
Inside the hook that watches `value`, if the value changed and the
change came from the system (i.e. a programmatic change) refresh the
editor by adjusting its key prop. If the change was made by the user,
there is no need to refresh the editor.
Fixes https://github.com/payloadcms/payload/issues/10819
Continuation of https://github.com/payloadcms/payload/pull/11372 but for our collection, global and auth operations
Previously, we were quite frequently using `.reduce()` to run hooks. This PR replaces them with simple `for` loops, which is less overhead, less code, less confusing and simpler to understand.
Continuation of #11008. When `filterOptions` are set on a relationship
field that is _nested within another field_, those filter options are
not applied to `Filter` component in the list view. This is because we
were only shallowly resolving filter options on top-level fields, as
opposed to recursively traversing fields to resolve them even when
deeply nested.
Updated `formatBreadcrumb` to fall back to an empty string if the
`useAsTitle` field for the document is undefined.
This handles cases where the field is optional or not filled out,
ensuring the label is never `undefined`.
Fixes#10377
This PR ensures that when `titleField.label` is provided as an object,
it is correctly translated and displayed in the search filter's
placeholder.
Previously, the implementation only supported string values, which could
lead to issues with object type labels. With these changes, object type
labels will now properly show as intended in the search filter.
Fixes#11348
Previously, we were quite frequently using `.reduce()` to sequentially run field hooks. This PR replaces them with simple `for` loops, which is less overhead, less code, less confusing and simpler to understand.
Additionally, it refactors `mergeLocaleActions` which previously was unnecessarily complex. They no longer entail async code, thus we no longer have to juggle with promises
When blocks have custom row labels, those row labels become stale when
reordering blocks. After moving a block, for example, the row label will
jump back the original block until form state returns with the proper
rendering order. This is especially evident on slow networks.
### What?
Two things:
1. Users unassigned to a tenant could not access their own account
2. Custom `tenantsArrayFieldName` and `tenantsArrayTenantFieldName`
configurations were not being used in all cases
### Why?
1. The access constraint provided by the plugin would not allow them to
make changes to their own account
2. `getUserTenantIDs` and `afterTenantDelete` were not using the custom
field names properly
### How?
1. Adds constraint for users allowing them to manage their own account
by default. Externally nothing has changed. If you need to lock your
users access control down you should do that just as you would without
this plugin.
2. Threads the field names through for usage.
Fixes https://github.com/payloadcms/payload/issues/11317
### What?
The `plugin-nested-docs` returns an array of breadcrumbs - the
`resaveChildren` file accidentally processed the breadcrumbs twice, once
where the data is updated and once within the `populateBreadcrumbs`
function which was causing the objects to be double nested.
### How?
Removes the extra nesting from `resaveChildren` file and allows the
`populateBreadcrumbs` to return the final data.
Fixes#10855
### What?
Information that locale fields in database are changing to a simpler
data structure in v3.
### Why?
Simple data migration is not enough to get it working, I had to spend
quite some time to figure out migration files and still it required some
manual input. Maybe others will find it useful when starting a v3
migration.
### How?
I had to do some custom migration scripts on my own to get v3 to work
with existing pages data.
---------
Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>