### 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`.
Fixes https://github.com/payloadcms/payload/issues/6884
Adds a new flag `acceptIDOnCreate` that allows you to thread your own
`id` to `payload.create` `data`, for example:
```ts
// doc created with id 1
const doc = await payload.create({ collection: 'posts', data: {id: 1, title: "my title"}})
```
```ts
import { Types } from 'mongoose'
const id = new Types.ObjectId().toHexString()
const doc = await payload.create({ collection: 'posts', data: {id, title: "my title"}})
```
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.
- Introduces a new lexical => plaintext converter
- Introduces a new lexical <=> markdown converter
- Restructures converter docs. Each conversion type gets its own docs
pag
Similar to https://github.com/payloadcms/payload/pull/11631.
IndentFeature causes pressing Tab in the middle of a block such as a
paragraph or heading to insert a TabNode. This property allows you to
disable this behavior, and indentation will occur instead if the block
allows it.
Usage:
```ts
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
IndentFeature({
disableTabNode: true,
}),
],
}),
```
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
Fixes
https://github.com/payloadcms/payload/issues/11731#issue-2923088087
Only 2 s3Storage() instances with `clientUploads: true` are currently
working. If you add a 3rd instance, uploading files errors with
"Collection credit-certificates was not found in S3 options".
There is already an implementation that changes the
`/storage-s3-generate-signed-url` URL for each new s3Storage plugin
instance so that multiple instances don't break each other. Currently
the code looks like this:
```ts
/**
* Tracks how many times the same handler was already applied.
* This allows to apply the same plugin multiple times, for example
* to use different buckets for different collections.
*/
let handlerCount = 0
for (const endpoint of config.endpoints) {
if (endpoint.path === serverHandlerPath) {
handlerCount++
}
}
if (handlerCount) {
serverHandlerPath = `${serverHandlerPath}-${handlerCount}`
}
```
When we print the endpoints generated by this code we get:
```ts
{
handler: [AsyncFunction (anonymous)],
method: 'post',
path: '/storage-s3-generate-signed-url'
},
{
handler: [AsyncFunction (anonymous)],
method: 'post',
path: '/storage-s3-generate-signed-url-1'
},
{
handler: [AsyncFunction (anonymous)],
method: 'post',
path: '/storage-s3-generate-signed-url-1'
}
```
As you can see, the path or the 3rd instance is the same as the 2nd
instance. Presumably this functionality was originally tested with 2
instances and not more, allowing this bug to slip through. This
completely breaks uploads for the 3rd instance.
We need to change the conditional that checks whether the plugin exists
already to use `.startsWith()`:
```ts
/**
* Tracks how many times the same handler was already applied.
* This allows to apply the same plugin multiple times, for example
* to use different buckets for different collections.
*/
let handlerCount = 0
for (const endpoint of config.endpoints) {
// We want to match on 'path', 'path-1', 'path-2', etc.
if (endpoint.path?.startsWith(serverHandlerPath)) {
handlerCount++
}
}
if (handlerCount) {
serverHandlerPath = `${serverHandlerPath}-${handlerCount}`
}
```
With the fix we can see that the endpoints increment correctly, allowing
for a true arbitrary number of s3Storage plugins with client uploads.
```ts
{
handler: [AsyncFunction (anonymous)],
method: 'post',
path: '/storage-s3-generate-signed-url'
},
{
handler: [AsyncFunction (anonymous)],
method: 'post',
path: '/storage-s3-generate-signed-url-1'
},
{
handler: [AsyncFunction (anonymous)],
method: 'post',
path: '/storage-s3-generate-signed-url-2'
}
```
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.
Sometimes I need to add some classes to the `picture` tag of Media
component. in this case I need to do this:
```
<Media
resource={content.image}
className="w-full h-full [&>picture]:w-full" // <<< follow this
imgClassName="w-full h-full object-cover"
/>
```
So I added an additional props `pictureClassName` for the picture tag.
Now I can do this:
```
<Media
resource={content.image}
className="w-full h-full"
pictureClassName="w-full h-full" // <<< follow this
imgClassName="w-full h-full object-cover"
/>
```
NOTE: I've encountered situations where I needed to add classes to the
`picture` tag, not just for `w-full h-full`. To handle this, I had to
update the Media component. I believe this would be a valuable
improvement to the Media component.
### What?
Adds a new property to collection / global config `forceSelect` which
can be used to ensure that some fields are always selected, regardless
of the `select` query.
### Why?
This can be beneficial for hooks and access control, for example imagine
you need the value of `data.slug` in your hook.
With the following query it would be `undefined`:
`?select[title]=true`
Now, to solve this you can specify
```
forceSelect: {
slug: true
}
```
### How?
Every operation now merges the incoming `select` with
`collectionConfig.forceSelect`.
Based on the current `packages/ui/src/providers/DocumentInfo/types.ts`.
- Removes properties like `versions`, `unpublishedVersions`,
`publishedDoc`, `getVersions` from the docs, which were listed in the
docs, but not in the type definition.
- Adds properties like `savedDocumentData`, `setCurrentEditor`,
`setDocFieldPreferences`, etc., which are in the type definition, but
which were missing in the docs.
- Fixes that the description for `getDocPermissions` said it retrieves
"user preferences", but should be about permissions.
### 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.
Ensures all s3 sockets are cleaned up. Now passes through default
request handler options that `@smithy/node-http-handler` now handles
properly.
Fixes#6382
```ts
const defaultRequestHandlerOpts: NodeHttpHandlerOptions = {
httpAgent: {
keepAlive: true,
maxSockets: 100,
},
httpsAgent: {
keepAlive: true,
maxSockets: 100,
},
}
```
If you continue to have socket issues, you can customize any of the
options by setting `requestHandler` property on your s3 config. This
will take precedence if set.
```ts
requestHandler: {
httpAgent: {
maxSockets: 300,
keepAlive: true,
},
httpsAgent: {
maxSockets: 300,
keepAlive: true,
},
// Optional, only set these if you continue to see issues. Be wary of timeouts if you're dealing with large files.
// time limit (ms) for receiving response.
requestTimeout: 5_000,
// time limit (ms) for establishing connection.
connectionTimeout: 5_000,
}),
```
<!--
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:

This PR fixes an issue where the Sharp `.resize()` function would round
down an auto-scaled dimension when `fastShrinkOnLoad` was enabled
(enabled by default).
This caused slight discrepancies in height calculations in certain edge
cases.
Be default (`fastShrinkOnLoad: true`), Sharp:
- Uses the built-in shrink-on-load feature for JPEG and WebP
- It is an optimization that prioritizes speed over precision when
resizing images
By setting `fastShrinkOnLoad: false`, we force Sharp to:
- Perform a more accurate resize operation instead of relying on quick
pre-shrink methods.
### Before / Context:
- Upload an image with original dimensions of 1500 × 735
- Define an `imageSize` of the following:
```
{
name: 'thumbnail',
width: 300,
},
```
#### Calculation:
`originalAspectRatio = 1500 / 735 ≈ 2.04081632653`
`resizeHeight = 300 / 2.04081632653`
`resizeHeight = 147`
However, Sharp's `.resize()` calculation would output:
`resizeHeight = 146`
This lead to an error of:
```
[17:05:13] ERROR: extract_area: bad extract area
err: {
"type": "Error",
"message": "extract_area: bad extract area",
"stack":
Error: extract_area: bad extract area
}
```
### After:
Sharp's `.resize()` calculation now correctly outputs:
`resizeHeight = 147`
### What?
Improves Swedish translations throughout.
- There were several places where the automatic translations didn't make
sense, particularily around localization where "locale" was incorrectly
referred to as "Lokal" instead of "Språk". "Crop" being translated to
"Skörd" was another hilarious one ("Skörd" means crop as in harvest 😊).
- Most success messages were overly formal in Swedish with
"framgångsrikt" being used in an awkward fashion. I've shortened them to
be more to the point.
- Some shorter strings had incorrect capitalization, such as "Nytt
Lösenord". Swedish doesn't use that kind of capitalization, so "Nytt
lösenord" is correct.
- Replaced "Manöverpanel" as the word for "Dashboard" with "Översikt"
which is less awkward.
- Normalized loading toasts throughout so they all use dots at the end
to signify an ongoing action such as "Laddar..".
- Several other small improvements to make things more natural.
The blocks e2e tests were flaky due to how we conditionally render
fields as they enter the viewport. This prevented Playwright from every
reaching the target element when running
`locator.scrollIntoViewIfNeeded()`. This is especially flaky on pages
with many fields because the page size would continually grow as it was
scrolled.
To fix this there are new `scrollEntirePage` and `waitForPageStability`
helpers. Together, these will ensure that all fields are rendered and
fully loaded before we start testing. An early attempt at this was made
via `page.mouse.wheel(0, 1750)`, but this is an arbitrary pixel value
that is error prone and is not future proof.
These tests were also flaky by an attempt to trigger a form state action
before it was ready to receive events. The fix here is to disable any
buttons while the form is initializing and let Playwright wait for an
interactive state.
allow disabling indentation for specific nodes via IndentFeature
Usage:
```ts
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
IndentFeature({
// the array must contain the "type" property of registered indentable nodes
disabledNodes: ['paragraph', 'listitem'],
}),
],
}),
```
The nodes "paragraph", "heading", "listitem", "quote" remain indentable
by default, even without `IndentFeature` registered.
In a future PR we will probably add the option to disable TabNode.
This bumps all eslint packages, ensuring compatibility with TypeScript 5.7.3. Previously, the following would be thrown:
```bash
WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.
You may find that it works just fine, or you may not.
SUPPORTED TYPESCRIPT VERSIONS: >=4.7.4 <5.7.0
YOUR TYPESCRIPT VERSION: 5.7.3
Please only submit bug reports when using the officially supported version
```
This [might have caused errors during linting](https://payloadcms.slack.com/archives/C04H7CQ615K/p1741707183505329?thread_ts=1741707036.030089&cid=C04H7CQ615K).
`payload` lint before: ✖ 380 problems (9 errors, 371 warnings)
`payload` lint after: ✖ 381 problems (9 errors, 372 warnings)
`ui` lint before: ✖ 154 problems (12 errors, 142 warnings)
`ui` lint after: ✖ 267 problems (12 errors, 255 warnings)
The additional warnings in `ui` come from the new `@eslint-react/no-use-context` and `@eslint-react/no-context-provider` rules which are good to have in React 19.
We now have the ability to define all page metadata for the admin panel
via the Payload Config as a result of #11593. This means we can now set
sensible defaults for additional properties, e.g. `noindex` and
`nofollow` on the `robots` property. Setting this will prevent these
pages from being indexed and appearing in search results.
Note that setting this property prevents _indexing_ these pages, but
does not prevent them from being _crawled_. To prevent crawling as well,
you must add a standalone `robots.txt` file to your root directory.
This simplifies and cleans up import map generation and adds support for turbopack, as well as the tsconfig `compilerOptions.basePath` property.
Previously, relative import paths looked like this:
```ts
import { TestComponent as ___ } from 'test/admin/components/TestComponent.js'
```
Paths like these will be resolved based on the `compilerOptions.baseUrl` path of your tsconfig.
This had 2 problems:
### baseUrl support
If your tsconfig baseUrl was not `"."`, this did not work, as the import map generator does not respect it
### Turbopack support
If Turbopack was used, certain import paths were not able to be resolved.
For example, if your component is outside the `baseDir`, the generated path looked like this:
```ts
import { TestComponent as ___ } from '/../test/admin/components/TestComponent.js'
```
This works fine in webpack, but breaks in turbopack.
## Solution
This PR ensures all import paths are relative, making them more predictable and reliable.
The same component will now generate the following import path which works in Turbopack and if a different `compilerOptions.basePath` property is set:
```ts
import { TestComponent as ___ } from '../../../test/admin/components/TestComponent.js'
```
It also adds unit tests