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.
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.
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.
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.
Previously, `db.updateVersion` had a mistake with using `transform({
operation: 'write' })` instead of `transform({ operation: 'read' })`
which led to improper DB data sanitization (like ObjectID -> string,
Date -> string) when calling `payload.update` with `autosave: true` when
some other autosave draft already exists. This fixes
https://github.com/payloadcms/payload/issues/11542 additionally for this
case.
### What? Cannot generate GraphQL schema with hyphenated field names
Using field names that do not adhere to the GraphQL `_a-z & A-Z`
standard prevent you from generating a schema, even though it will work
just fine everywhere else.
Example: `my-field-name` will prevent schema generation.
### How? Field name sanitization on generation and querying
This PR adds sanitization to the schema generation that sanitizes field
names.
- It formats field names in a GraphQL safe format for schema generation.
**It does not change your config.**
- It adds resolvers for field names that do not adhere so they can be
mapped from the config name to the GraphQL safe name.
Example:
- `my-field` will turn into `my_field` in the schema generation
- `my_field` will resolve from `my-field` when data comes out
### Other notes
- Moves code from `packages/graphql/src/schema/buildObjectType.ts` to
`packages/graphql/src/schema/fieldToSchemaMap.ts`
- Resolvers are only added when necessary: `if (formatName(field.name)
!== field.name)`.
---------
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
### 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`
This change makes so that data that exists in MongoDB but isn't defined
in the Payload config won't be included to `payload.find` /
`payload.db.find` calls. Now we strip all the additional keys.
Consider you have a field named `secretField` that's also `hidden: true`
(or `read: () => false`) that contains some sensitive data. Then you
removed this field from the database and as for now with the MongoDB
adapter this field will be included to the Local API / REST API results
without any consideration, as Payload doesn't know about it anymore.
This also fixes https://github.com/payloadcms/payload/issues/11542 if
you removed / renamed a relationship field from the schema, Payload
won't sanitize ObjectIDs back to strings anymore.
Ideally you should create a migration script that completely removes the
deleted field from the database with `$unset`, but people rarely do
this.
If you still need to keep those fields to the result, this PR allows you
to do this with the new `allowAdditionalKeys: true` flag.
This adds a new `disableAutoLinks` property to the `LinkFeature` that lets you disable the automatic creation of links while typing them in the editor or pasting them.
Deprecates the old HTML converter and introduces a new one that functions similarly to our Lexical => JSX converter.
The old converter had the following limitations:
- It imported the entire lexical bundle
- It was challenging to implement. The sanitized lexical editor config had to be passed in as an argument, which was difficult to obtain
- It only worked on the server
This new HTML converter is lightweight, user-friendly, and works on both server and client. Instead of retrieving HTML converters from the editor config, they can be explicitly provided to the converter function.
By default, the converter expects populated data to function properly. If you need to use unpopulated data (e.g., when running it from a hook), you also have the option to use the async HTML converter, exported from `@payloadcms/richtext-lexical/html-async`, and provide a `populate` function - this function will then be used to dynamically populate nodes during the conversion process.
## Example 1 - generating HTML in your frontend
```tsx
'use client'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
import React from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({ data })
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## Example - converting Lexical Blocks
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
})
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## Example 3 - outputting HTML from the collection
```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
import type { MyTextBlock } from '@/payload-types.js'
import type { CollectionConfig } from 'payload'
import {
BlocksFeature,
type DefaultNodeTypes,
lexicalEditor,
lexicalHTMLField,
type SerializedBlockNode,
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor(),
},
lexicalHTMLField({
htmlFieldName: 'nameOfYourRichTextField_html',
lexicalFieldName: 'nameOfYourRichTextField',
}),
{
name: 'customRichText',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
interfaceName: 'MyTextBlock',
slug: 'myTextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
}),
],
}),
},
lexicalHTMLField({
htmlFieldName: 'customRichText_html',
lexicalFieldName: 'customRichText',
// can pass in additional converters or override default ones
converters: (({ defaultConverters }) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
})) as HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<MyTextBlock>>,
}),
],
}
```
https://github.com/payloadcms/payload/pull/10794 added new defaults the config - however, these were only added to the deprecated `defaults` object, which caused our CI to fail. This PR adds them to the new `addDefaultsToConfig` function
This PR adds a top-level `auth` property to the Payload config, where
you can specify a new `jwtOrder` property to dictate, in Payload's local
auth strategy, which JWT extraction methods should be leveraged, and in
which order.
For example, we currently use incoming request headers to retrieve a JWT
in the following order:
1. If there is an `Authorization: JWT ${token}` header
2. If there is an `Authorization: Bearer ${token}` header
3. If there is an HTTP-only cookie with a token present
Now you can define which of these strategies you'd like to support, and
in which order.
Todo:
- [ ] Docs
- [ ] Tests
Previously when `authenticate` method from an authentication strategy
failed it stopped execution of the current request in
`createPayloadRequest` which isn't a good behavior.
Right now it completely prevents the admin panel from loading:
<img width="637" alt="image"
src="https://github.com/user-attachments/assets/7a6ca006-7457-4f9f-8746-7b3f52d65583"
/>
Now, each `strategy.authenticate` call is wrapped into `try` / `catch`,
if an error happens we use `logError` to correctly log that error by its
logging level.
Imports https://github.com/payloadcms/payload-admin-bar into the Payload
monorepo. This package will now be regularly maintained directly
alongside all Payload packages and now includes its own test suite.
A few changes minor have been made between v1.0.7 and latest:
1. The package name has changed from `payload-admin-bar` to
`@payloadcms/admin-bar`.
```diff
- import { PayloadAdminBar } from 'payload-admin-bar'
+ import { PayloadAdminBar } from '@payloadcms/admin-bar'
```
2. The `collection` prop has been renamed to `collectionSlug`
3. The `authCollection` prop has been renamed to `authCollectionSlug`
Here's a screenshot of the admin bar in use within the Website Template:
<img width="1057" alt="Screenshot 2025-03-05 at 1 20 04 PM"
src="https://github.com/user-attachments/assets/2597a8fd-da75-4b2f-8979-4fc8132999e8"
/>
---------
Co-authored-by: Kalon Robson <kalon.robson@outlook.com>
Fixes https://github.com/payloadcms/payload/issues/11473
Previously, when `disablePayloadAccessControl: true` was defined, client
uploads were working improperly. The reason is that
`addDataAndFileToRequest` expects `staticHandler` to be defined and we
don't add in case if `disablePayloadAccessControl: true`.
This PR makes it so otherwise and if we have `clientUploads`, it pushes
the "proxied" handler that responses only when the file was requested in
the context of client upload (from `addDataAndFileToRequest`)
### What?
Regression caused by https://github.com/payloadcms/payload/pull/11433
If a beforeChange hook was checking for a missing or undefined `value`
in order to change the value before inserting into the database, data
could be lost.
### Why?
In #11433 the logic for setting the fallback field value was moved above
the logic that cleared the value when access control returned false.
### How?
This change ensures that the fallback value is passed into the
beforeValidate function _and_ still available with the fallback value on
siblingData if access control returns false.
Fixes https://github.com/payloadcms/payload/issues/11543