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
### What?
The Lithuanian i18n translations have the placeholders (i.e.
`{{label}}`) also translated. For example `{{label}}` to `{{žymė}}`
My guess is that this was caused by the `pnpm translateNewKeys` script
which feeds all of the strings to OpenAI. In the system message in
[translateText.ts#L15](https://github.com/payloadcms/payload/blob/main/packages/translations/scripts/translateNewKeys/translateText.ts#L15)
there is nothing mentioning that it should not translate placeholders.
But I guess the AI was clever enough most of the time and not translated
them, leaving them as is. Because in the Lithuanian translation most
placeholders were correctly left as is, but a couple of them weren't.
I would have updated the system message, but I struggled to setup my
environment so that `pnpm translateNewKeys` would work (probably because
I'm on windows, idk). So I'm leaving the system message as is because I
can't test my changes, someone else should update it in another PR.
### Why?
Lithuanian messages weren't translated correctly.
### How?
Manually went through all of the used placeholders in in `lt.ts` and
updated the ones which were translated. Double checked using `en.ts`
file to see what was the original placeholder name.
Fixes#9858
# The problems
There were several issues with custom i18n typing in the documentation
that were not detected because they did not occur in non-strict ts mode.
1. `Config['i18n']['translations']` didn't work, because i18n is an
optional property. As described in
[#9858](https://github.com/payloadcms/payload/issues/9858#issuecomment-2555814771),
some users were getting around this with
`NonNullable<Config['i18n']>['translations']`
2. [The trick being attempted in
`i18n`](36e152d69d/packages/payload/src/config/types.ts (L1034))
to customize and extend the `DefaultTranslationObject` does not work.
`i18n?: I18nOptions<{} | DefaultTranslationsObject> // loosen the type
here to allow for custom translations`.
If you want to verify this, you can use the following code example:
```ts
import type { Config } from 'payload'
const translation: NonNullable<Config['i18n']>['translations'] = {
en: {
authentication: {
aaaaa: 'aaaaa', // I chose `authentication.aaaa` to appear first in intellisense
}
},
}
translation.en?.authentication // Property 'authentication' does not
// exist on type '{} | { authentication: { account: string...
// so this option doesn't let you access the keys because of the join with `{}`,
// and even if it did, it's not adding `aaaa` as a key.
```
3. In places where the `t` function is exposed in a callback, you cannot
do what the documentation says:
`{ t }: { t: TFunction<CustomTranslationsKeys | DefaultTranslationKeys>
}`
The reason for this is that the callback is exposed as a `LabelFunction`
type but without type arguments, and as a default it uses
`DefaultTranslationKeys`, which does not allow additional keys.
If you want to verify this, you can use the following code example:
```ts
// Make sure to test this with ts in strict mode
const _labelFn: LabelFunction = ({ t }: { t: TFunction<'extraKey' | DefaultTranslationKeys> }) => ""
// Type '"extraKey"' is not assignable to type
// '"authentication:account" | ... 441 more ... | "version:versionCount"'.
```
# The solution
Point 1 is a documentation issue. We could use `NonNullable`, or expose
the `I18nOptions` type, or simply not define the custom translation type
(which makes sense because if you put it in the config, ts will warn you
anyway).
Points 2 and 3 should ideally be corrected at the type level, but it
would imply a breaking change.
For now, I have corrected them at the documentation level, using an
alternative for point 2 and a type cast for point 3.
Maybe in payload v4 we should revisit this.
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
Payload now fully exposes Next.js' metadata options. You can now use the
`admin.meta` config to set any properties that Next.js supports and
Payload will inject them into its `generateMetadata` function call. The
`MetaConfig` provided by Payload now directly extends the `Metadata`
type from Next.js.
Although `admin.meta` has always been available, it only supported a
subset of options, such as `title`, `openGraph`, etc., but was lacking
properties like `robots`, etc.
### What?
When the upload config contains imageSizes, we are forcing the image to
be resized using sharp. This leads to lossy compression even when the
formatOptions and no cropping or focal point selection was made. This
change makes it possible to upload the original image, skipping
compression while still using the imageSizes feature.
### Why?
It should be possible to upload files without compression.
### How?
Changes the conditions to remove imageSizes to determine if sharp image
processing should be applied to the original image or not.