### What?
When a user lands on an edit page that has a relationship to an `Upload`
field (which is `HasMany`). The UI will make a request with `limit=0` to
the backend providing there is no IDs populated already.
When a media collection is large, it will try and load all media items
into memory which causes OOM crashes.
### Why?
Fixes: https://github.com/payloadcms/payload/issues/11655 causing OOM
issues.
### How?
Adding guard check on the `populate` to ensure that it doesn't make a
request if not needed.
https://github.com/user-attachments/assets/f195025f-3e31-423e-b13e-6faf8db40129
### What?
This PR fixes a bug in the relationship filter UI where no options are
displayed when working in a non-default locale with localized
collections. The query to fetch relationship options wasn't including
the current locale parameter, causing the select dropdown to appear
empty.
### Why?
When using localized collections with relationship fields:
1. If you create entries (e.g., Categories) only in a non-default locale
2. Set the global locale to that non-default locale
3. Try to filter another collection by its relationship to those
Categories
The filter dropdown would be empty, despite Categories existing in that
locale. This was happening because the `loadOptions` method in the
RelationshipFilter component didn't include the current locale in its
query.
### How?
The fix is implemented in
`packages/ui/src/elements/WhereBuilder/Condition/Relationship/index.tsx`
by:
1. Adding the `useLocale` hook to get the current locale in the
RelationshipFilter component
2. Including this locale in the query parameters when fetching
relationship options


Fixes#11782
Discussion:
https://discord.com/channels/967097582721572934/1350888604150534164
Fixes#10019. When bulk editing subfields, such as a field within a
group, changes are not persisted to the database. Not only this, but
nested fields with the same name as another selected field are
controlled by the same input. E.g. typing into one fields changes the
value of both.
The root problem is that field paths are incorrect.
When opening the bulk edit drawer, fields are flattened into options for
the field selector. This is so that fields in a tab, for example, aren't
hidden behind their tab when bulk editing. The problem is that
`RenderFields` is not set up to receive pre-determined field paths. It
attempts to build up its own field paths, but are never correct because
`getFieldPaths` receives the wrong arguments.
The fix is to just render the top-level fields directly, bypassing
`RenderFields` altogether.
Fields with subfields will still recurse through this function, but at
the top-level, fields can be sent directly to `RenderField` (singular)
since their paths have already been already formatted in the flattening
step.
### What
This PR updates the `login` flow by wrapping redirect routes with
`encodeURIComponent`. This ensures that special characters in URLs (such
as ?, &, #) are properly encoded, preventing potential issues with
navigation and redirection.
If the `useAsTitle` property is defined referencing a richtext field,
the admin panel throws errors in several places.
I noticed this in the email builder plugin, where we're making the
subject field (which is the title) a single-paragraph richtext field
instead of a text field for technical reasons.
In this PR, for the lexical richtext case, I'm converting the first
child of the RootNode (usually a paragraph or heading) to plain text.
Additionally, I am verifying that if the resulting title is not of type
string, fallback to "untitled" so that this does not happen again in the
future (perhaps with slate, or with other fields).
### What?
This PR ensures that bulk uploads fail if any file is missing, rather
than skipping missing files and proceeding with the upload.
### Why?
This fixes unintended behavior where missing files were skipped,
allowing partial uploads when they shouldn't be allowed.
### How?
- Prevents submission if any file is missing by checking `req.status ===
400`.
- Updates `FileSidebar` to correctly handle cases where a file is
`null`.
Bulk edit can now request a partial form state thanks to #11689. This
means that we only need to build form state (and send it through the
network) for the currently selected fields, as opposed to the entire
field schema.
Not only this, but there is no longer a need to filter out unselected
fields before submitting the form, as the form state will only ever
include the currently selected fields. This is unnecessary processing
and causes an excessive amount of rendering, especially since we were
dispatching actions within a `for` loop to remove each field. React may
have batched these updates, but is bad practice regardless.
Related: stripping unselected fields was also error prone. This is
because the `overrides` function we were using to do this receives
`FormState` (shallow) as an argument, but was being treated as `Data`
(not shallow, what the create and update operations expect).
E.g. `{ myGroup.myTitle: { value: 'myValue' }}` → `{ myGroup: { myTitle:
'myValue' }}`.
This led to the `sanitizeUnselectedFields` function improperly
formatting data sent to the server and would throw an API error upon
submission. This is only evident when sanitizing nested fields. Instead
of converting this data _again_, the select API takes care of this by
ensuring only selected fields exist in form state.
Related: bulk upload was not hitting form state on change. This means
that no field-level validation was occurring on type.
Fixes a problem where we would reset the value of the timezone field on
submission of a new scheduled publish.
Timezones in the table now match with a label if possible.

`Australia/Brisbane` is now part of the default list of timezones
The form component's `initializing` and `processing` states do not
disable fields that are rendered outside of `DocumentFields`. Fields
currently rely on the `readOnly` prop provided by `DocumentFields` and
do not subscribe to these states for themselves. This means that fields
that are rendered outright, such as within the bulk edit drawer, they do
not receive a `readOnly` prop and are therefore never disabled.
The fix is add a `disabled` property to the `useField` hook. This
subscribes to the `initializing` and `processing` states in the same way
as `DocumentFields`, however, now each field can determine its own
disabled state instead of relying solely on the `readOnly` prop. Adding
this new prop has no overhead as `processing` and `initializing` is
already being subscribed to within `useField`.
### What?
This PR optimizes how images are selected for display in the upload list
view. It ensures that only image files are processed and selects the
most appropriate size to minimize unnecessary downloads and improve
performance.
#### Previously:
- Non-image files were being processed unnecessarily, despite not
generating thumbnails.
- Images without a `thumbnailURL` defaulted to their original full size,
even when smaller, optimized versions were available.
#### Now:
- **Only images** are processed for thumbnails, avoiding redundant
requests for non-images.
- **The smallest available image within a target range** (`40px -
180px`) is prioritized for display.
- **If no images fit within this range**, the logic selects:
- The next smallest larger image (if available).
- The **original** image if it is smaller than the next available larger
size.
- The largest **smaller** image if no better fit exists.
### Why?
Prevents unnecessary downloads of non-image files, reduces bandwidth
usage by selecting more efficient image sizes and improves load times
and performance in the list view.
### How?
- **Filters out non-image files** when determining which assets to
display.
- Uses a more precise selection algorithm to find the best-fit image
size:
- Prefers the smallest image within `40px - 180px`.
- Falls back to the closest match above or below the range if no
in-range image exists.
- Ensures the original image is only used when it provides a better fit.
Fixes#11690
Before (4.7mb transfer):

After (129kb transfer):

Implements a select-like API into the form state endpoint. This follows
the same spec as the Select API on existing Payload operations, but
works on form state rather than at the db level. This means you can send
the `select` argument through the form state handler, and it will only
process and return the fields you've explicitly identified.
This is especially useful when you only need to generate a partial form
state, for example within the bulk edit form where you select only a
subset of fields to edit. There is no need to iterate all fields of the
schema, generate default values for each, and return them all through
the network. This will also simplify and reduce the amount of
client-side processing required, where we longer need to strip
unselected fields before submission.
### What
This PR ensures that `select` and `radio` field option labels properly
accept and render JSX elements.
### Why
Previously, JSX elements could be passed as option labels, but the type
definition for options only allowed `LabelFunction` or `StaticLabel`,
resulting in type errors. Additionally:
- JSX labels did not render correctly in the list view but now do.
- In the versions diff view, JSX labels were not supported since it only
accepts strings. To address this, we now fallback to the option `value`
when the label is a JSX element.
### What?
Prevents the preventLeave dialog from showing if a clicked link is about
to open in a new tab.
### Why?
Currently, no external link can be clicked on the edit page if it was
modified, even if the link would not navigate the user away from that
page but open in a new tab instead.
### How?
We don't trigger the preventLeave dialog if
- the target of a clicked anchor is `_blank`
- the user pressed the command or ctrl key while clicking on a link
(which opens link in a new tab)
The task queue triggers an infinite render of form state. This is
because we return an object from the `useQueues` hook that is recreated
on every render. We then use the `queueTask` function as an unstable
dependency of the `useEffect` responsible for requesting new form state,
ultimately triggering an infinite rendering loop.
The fix is to stabilize the `queueTask` function within a `useCallback`.
Adds a test to prevent future regression.
<!--
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?
The error toast shown on field errors was _greatly_ improved recently
with much clearer, more easily consumable messages. This PR adjusts a
minor issue when the format of the error message is such that there are
no subsequent field errors present.
### Why?
To prevent showing an extra `li` when there are no more field errors.
### How?
Previously, the error msg array was being constructed like so:
```ts
const [intro, errorsString] = message.split(':')
const errors = (errorsString || '')
.split(',')
.map((error) => error.replaceAll(' > ', ' → ').trim())
if (errors.length === 0) {
return {
message: intro,
}
}
...
```
This works fine. However, if the initial message split makes
`errorsString` undefined, as is the case where there are no subsequent
field errors, the `(errorsString || '').split(',')` will always return
an array with a single `""` element in it, making the check for
`errors.length === 0` unreachable. This PR checks if `errorsString` is
false-y first before doing further processing instead.
Before:

After:

Implements a form state task queue. This will prevent onChange handlers
within the form component from processing unnecessarily often, sometimes
long after the user has stopped making changes. This leads to a
potentially huge number of network requests if those changes were made
slower than the debounce rate. This is especially noticeable on slow
networks.
Does so through a new `useQueue` hook. This hook maintains a stack of
events that need processing but only processes the final event to
arrive. Every time a new event is pushed to the stack, the currently
running process is aborted (if any), and that event becomes the next in
the queue. This results in a shocking reduction in the time it takes
between final change to form state and the final network response, from
~1.5 minutes to ~3 seconds (depending on the scenario, see below).
This likely fixes a number of existing open issues. I will link those
issues here once they are identified and verifiably fixed.
Before:
I'm typing slowly here to ensure my changes aren't debounce by the form.
There are a total of 60 characters typed, triggering 58 network requests
and taking around 1.5 minutes to complete after the final change was
made.
https://github.com/user-attachments/assets/49ba0790-a8f8-4390-8421-87453ff8b650
After:
Here there are a total of 69 characters typed, triggering 11 network
requests and taking only about 3 seconds to complete after the final
change was made.
https://github.com/user-attachments/assets/447f8303-0957-41bd-bb2d-9e1151ed9ec3
This PR updates the field `validate` function property to include a new
`path` argument.
The `path` arg provides the schema path of the field, including array
indices where applicable.
#### Changes:
- Added `path: (number | string)[]` in the ValidateOptions type.
### What?
This [PR](https://github.com/payloadcms/payload/pull/11546) introduced a
bug where the `CopyToLocale` button can show up when localization is
false.
### Why?
`const disableCopyToLocale = localization &&
collectionConfig?.admin?.disableCopyToLocale` this line was faulty
### How?
Fixed the logic and added test to confirm button doesn't show when
localization is false.
### What?
We have the option to set `displayPreview: true || false` on upload
collections / upload fields - with the **field** option taking
precedence.
Currently, `displayPreview` is only affecting the list view for the
**_related_** collection.
i.e. if you go to a collection that has an upload field - the preview
will be hidden/shown correctly according to the `displayPreview` option.
<img width="620" alt="Screenshot 2025-03-03 at 12 38 18 PM"
src="https://github.com/user-attachments/assets/c11c2a84-0f64-4a08-940e-8c3f9096484b"
/>
However, when you go directly to the upload collection and look at the
list view - the preview is always shown, not affected by the
`displayPreview` option.
<img width="446" alt="Screenshot 2025-03-03 at 12 39 24 PM"
src="https://github.com/user-attachments/assets/f5e1267a-d98a-4c8c-8d54-93dea6cd2e31"
/>
Also, we have previews within the file field itself - also not being
affected by the `displayPreview` option.
<img width="528" alt="Screenshot 2025-03-03 at 12 40 06 PM"
src="https://github.com/user-attachments/assets/3dd04c9a-3d9f-4823-90f8-b538f3d420f9"
/>
All the upload related previews (excluding preview sizes and upload
editing options) should be affected by the `displayPreview` option.
### How?
Checks for `collection.displayPreview` and `field.displayPreview` in all
places where previews are displayed.
Closes#11404
Two small separate issues here (1) and (2):
### What?
1. Excess margin is displayed when a row is hidden due to
`admin.condition`
2. The `admin.style` props is never passed to the `row` field
### Why?
1. Unlike other fields, the `row` field still gets rendered when
`admin.condition` returns false - this is because the logic gets passed
down to the fields within the row
2. `style` was never being threaded to the `row` field wrapper
### How?
1. Hides the row using css to `display: none` when no children are
present
2. Passes `admin.styles` to the `row` wrapper
Fixes#11477
### What?
Adds new option to disable the `copy to locale` button, adds description
to docs and adds e2e test.
### Why?
Client request.
### How?
The option can be used like this:
```ts
// in collection config
admin: {
disableCopyToLocale: true,
},
```
Fixes https://github.com/payloadcms/payload/issues/11568
### What? Out of sync errors states
- Collaspibles & Tabs were not reporting accurate child error counts
- Arrays could get into a state where they would not update their error
states
- Slight issue with toasts
### Tabs & Collapsibles
The logic for determining matching field paths was not functioning as
intended. Fields were attempting to match with paths such as `_index-0`
which will not work.
### Arrays
The form state was not updating when the server sent back errorPaths.
This PR adds `errorPaths` to `serverPropsToAccept`.
### Toasts
Some toasts could report errors in the form of `my > > error`. This
ensures they will be `my > error`
### Misc
Removes 2 files that were not in use:
- `getFieldStateFromPaths.ts`
- `getNestedFieldState.ts`
Adds a new `admin.disableBlockName` property that allows you to disable
the blockName field entirely in the admin view. It defaults to false for
backwards compatibility.
This PR updates the field `condition` function property to include a new
`path` argument.
The `path` arg provides the schema path of the field, including array
indices where applicable.
#### Changes:
- Added `path: (number | string)[]` in the Condition type.
- Updated relevant condition checks to ensure correct parameter usage.
When rendering views within a drawer outside of the edit view, i.e. from
the list view, it updates the underlying step nav to the collection of
the drawer. This is true for both document drawers and list drawers.
This is because the logic controlling this behavior relies on the
current edit depth, which is only incremented within the edit view
itself. Instead of doing this, we can conditionally run the setter
functions based the presence of a drawer slug.
An alternative to this would be to subscribe to the `drawerDepth`
context but this would be less efficient, as this requires an
unnecessary hook and subsequent rendering cycle.
### What?
Supersedes https://github.com/payloadcms/payload/pull/11490.
Refactors imports of `formatAdminURL` to import from `payload/shared`
instead of `@payloadcms/ui/shared`. The ui package now imports and
re-exports the function to prevent this from being a breaking change.
### Why?
This makes it easier for other packages/plugins to consume the
`formatAdminURL` function instead of needing to implement their own or
rely on the ui package for the utility.
The "where" builder maintains its own duplicative state for conditions.
This is problematic when an outside force needs to control the
conditions in some way, but the "where" builder will not receive those
updates.
While it is a requirement of the "where" builder to transform the
"where" query into "and" / "or" format for rendering, it does so in a
way that causes it to become out of sync with the query provider. This
is because we first initialize state from context, then for every change
to conditions, report those updates to contexts—but not the other way
around.
To fix this, we need to completely remove state from the "where" builder
and solely rely on the query context as a single source of truth. This
will allow it to receive automatic updates from query provider without
needing to sync both local state and context simultaneously. Now, we
only ever need to send updates to the query provider and let the
top-down rendering cycle propagate those changes everywhere.
Previously, lexical blocks initialized a new `Form` component that rendered as `<form>` in the DOM. This may lead to React errors, as forms nested within forms is not valid HTML.
This PR changes them to render as `<div>` in the DOM instead.
When bulk editing an auth-enabled collection such as users, a
client-side exception is thrown. This is because we're trying to access
the `disableBulkEdit` property on `undefined`. This is due to hidden,
auth-specific fields like `salt` and `hash` lacking an admin config.
No test is explicitly needed for this as `"strictNullChecks": true` will
throw an error at compile time, once enabled.
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.