### What?
Adds `groupBy` field support to query presets. Presets now save, apply,
and clear groupBy state when switching between presets.
### Why?
Previously, `groupBy` settings would persist when switching between
presets. GroupBy was not being saved as part of the preset
configuration, so users couldn't save their preferred grouping with a
preset.
### How?
Added `groupBy?: string` to the `QueryPreset` type and updated the
preset collection config to include the field.
Modified `QueryPresetBar` to handle groupBy when selecting, deselecting,
resetting, saving, or creating presets.
Created custom Field and Cell components to display groupBy values in
the admin UI.
#### Preset creation with group-by added
<img width="711" height="684" alt="Screenshot 2025-12-03 at 3 15 03 PM"
src="https://github.com/user-attachments/assets/5fe6ee88-6ed7-40f6-b190-df6b2e89460c"
/>
#### Preset collection list view with group-by preset
<img width="2233" height="537" alt="Screenshot 2025-12-03 at 3 15 26 PM"
src="https://github.com/user-attachments/assets/a56645fc-9106-4885-8c54-0e000c429499"
/>
#### Preset creation without group-by
<img width="1250" height="704" alt="Screenshot 2025-12-03 at 3 15 48 PM"
src="https://github.com/user-attachments/assets/3438585b-7aa9-4336-9101-2fab59733480"
/>
### What?
This PR adds the missing `beforeInput` and `afterInput` properties to
the `UploadAdmin` type so that the type definition reflects the actual
supported admin props for upload fields.
### Why?
Although upload fields [already support `beforeInput` and
`afterInput`](https://github.com/payloadcms/payload/blob/fb93cd13bcd66dca3777826fc420a941d3cb75ab/packages/ui/src/fields/Upload/index.tsx#L51)
in practice, these props are currently not included in the `UploadAdmin`
TypeScript type. This leads to type errors or the need for type
assertions when trying to use them. Aligning the type definition with
the existing runtime behavior improves DX and keeps the admin field
types consistent, similar to what was done for RichTextField in #12889.
### How?
Extended the UploadAdmin type to include the `beforeInput` and
`afterInput` properties.
Fixes https://github.com/payloadcms/payload/issues/14726
Refactors how we call beforeOperation hooks more similarly to how we
call afterOperation hooks.
Adds beforeOperations and afterOperations functions to operations.
Both were missing in `unlock`, `findVersions` and `findVersionByID`
Updates the config.versions (on the sanitized config) to be proper.
#### Before
```ts
versions: SanitizedCollectionVersions // not accurate
```
#### After
```ts
versions?: SanitizedCollectionVersions
```
Previously sanitization did not remove the versions key when it was set
to false so the actual type should have been `versions:
SanitizedCollectionVersions | false`.
I chose to remove the versions key if it is false in the sanitization
function so it is easier to work with, i.e. optional chaining.
Also adds importable helper functions:
```ts
import {
getAutosaveInterval,
getVersionsMax,
hasAutosaveEnabled,
hasDraftsEnabled,
hasDraftValidationEnabled,
hasScheduledPublishEnabled,
} from 'payload/shared'
```
Continuation of #14007.
Supports overriding the default slugify function of the slug field. This
is necessary if the slug requires special treatment, such as character
encoding, additional language support, etc.
For example, if you wanted to use the
[`slugify`](https://www.npmjs.com/package/slugify) package, you could do
something like this:
```ts
import type { CollectionConfig } from 'payload'
import { slugField } from 'payload'
import slugify from 'slugify';
export const MyCollection: CollectionConfig = {
// ...
fields: [
// ...
slugField({
slugify: ({ valueToSlugify }) => slugify(valueToSlugify, {
// ...additional `slugify` options here
})
})
]
}
```
This PR also deprecates the old `fieldToUse` arg in favor of `useAsSlug`
which is more semantically clear, following the same convention as
`useAsTitle`.
Example:
```ts
import type { CollectionConfig } from 'payload'
import { slugField } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
fields: [
// ...
slugField({
useAsSlug: 'myCustomTitle'
})
]
}
```
In follow-up PRs, we should also:
- Improve the default slugify function to better handle special
characters on its own, etc.
- [Support nested
slugs](https://github.com/payloadcms/payload/pull/14783)
### What?
Fix bug of `svg` files throwing an error when `mimeTypes: ['image/*']`
is set.
### Why?
We recently added an additional mime type check on the buffer however
the `file-buffer` package that we use to detect file type does not
support svgs.
### How?
Updates `expectsDetectableType` logic to exclude svgs.
Fixes#14743
## Description
Enables the use of external JSON schema file references in type
generation by adding the `cwd` parameter to
`json-schema-to-typescript`'s `compile()` function.
## Changes
- Added `cwd: process.cwd()` parameter to `generateTypes()` compile call
- Enables `$ref` pointers to external `.json` schema files in
`typescriptSchema` field config
- Added test case with external schema file reference
- Documented external schema reference usage in generating-types.mdx
## Test Coverage
✅ New test: `resolves external schema file references`
- Created `test/types/schemas/custom-type.json` as external schema
- Added field with `typescriptSchema` referencing the external file
- Verified generated types include the external type correctly
## Example Usage
```typescript
// payload.config.ts
{
typescript: {
schema: [
({ jsonSchema }) => {
jsonSchema.definitions.MyType = {
$ref: './schemas/my-type.json'
}
return jsonSchema
},
]
}
}
```
External references are resolved relative to `process.cwd()`.
## Related Issue
This was identified during investigation of JSON Schema parser
capabilities. The `cwd` parameter is a standard
`json-schema-to-typescript` option that was not being passed.
### What?
Adds augmentable interfaces for the `custom` property on collections and
globals:
- `CollectionCustom`
- `CollectionAdminCustom`
- `GlobalCustom`
- `GlobalAdminCustom`
Also updates `plugin-import-export` to use `CollectionAdminCustom` for
its internal storage.
### Why?
Fields already have `FieldCustom` which plugins can augment for
type-safe custom config. Collections and globals were still using
`Record<string, any>` directly, so there was no way to get proper types
when building plugins that need collection/global-level configuration.
This made it annoying to build well-typed plugins since you'd lose all
type safety and autocomplete at the collection/global level.
### How?
- Added the four interfaces (same pattern as `FieldCustom` - empty
interfaces extending `Record<string, any>`)
- Updated the type definitions in `collections/config/types.ts` and
`globals/config/types.ts` to use them
- Updated `plugin-import-export` to augment `CollectionAdminCustom` so
it gets proper types for the `disabledFields` array it stores
Fixes#14724
### What?
Added execution of `beforeLogin` and `afterLogin` hooks during the
password reset operation in `resetPasswordOperation()`. These hooks now
execute with the same parameters and flow as they do in the standard
login operation, ensuring consistent behavior across both authentication
paths.
- `packages/payload/src/auth/operations/resetPassword.ts` - Added hook
execution before and after JWT token generation.
### Why?
When a user resets their password, the operation performs two phases:
updating the password in the database, then automatically logging the
user in with a new session and JWT token. However, `beforeLogin` and
`afterLogin` hooks were not being triggered during this auto-login
phase, creating inconsistent behavior compared to standard login.
This caused issues for applications that uses `afterLogin` hooks to
attach additional cookie (for example).
### How?
Triggered `beforeLogin` and `afterLogin` in `resetPassword.ts`
### What?
Updates the `restoreVersion` logic to ensure restoring as a draft does
not overwrite the currently published data.
### Why?
If you have a published doc, then save a draft, you expect to be able to
access the published data by `draft: false` and the draft by
`draft:true`. When you restore a version as a draft, the same behavior
is expected.
However, in the `restoreVersion` operation, we are updating the main doc
even if you are restoring as a draft. This overwrites the published
data, so now you will see draft data returned regardless of querying
with `draft: true/false`.
### How?
When restoring a version as a draft, we only need to save a new version.
The main doc should only be updated when restoring as published.
This change checks `(!draftArg)` before running the `updateOne` on the
main doc.
#### Reported by client.
`payload.countVersions` was not passing the locale through, so it was
not possible to query on localized fields without querying on the
localized path. This differed from how the find operation works.
In large projects, calculating the `permissions` object is one of the
slowest parts of Payload. This PR completely rewrites the permission
calculation system to make it significantly faster and more reliable.
The permissions object is calculated multiple times throughout Payload's
lifecycle: for the entire config every time we load or navigate to any
admin panel page, on every API endpoint call, and as part of query path
validation when calling any Payload operation.
This is done by `getAccessResults` without any document data, and in
large configs can be one of the slowest operations, noticeably slowing
down admin panel navigation.
After this rewrite, **speed improvements in permissions calculation
without data range from 5.3% (access-control test suite) to 74% (fields
test suite - our largest config)**. The larger the config, the more
noticeable these improvements become. These performance gains apply to
both the Payload Admin Panel and API requests.
---
The `permissions` object can also be calculated WITH document data,
which triggers evaluation of `Where` conditions in
collection/global-level access control and may fetch the document to
pass to access control functions.
This has been heavily optimized with **speed improvements for
permissions calculation with data ranging between 75% and 5138%** (if I
craft a collection config that excessively uses aspects this PR
improves) in the benchmarks. This optimization affects:
- Loading any document (collection/global edit view or drawer)
- Saving a document
- Loading the list view with query presets applied
- Bulk upload initialization (runs twice)
These improvements are most noticeable when navigating _to_ documents in
the admin panel.
## What's Fixed
In addition to performance improvements, the new function is now
cleaner, easier to understand and works more reliably. I specifically
wanna call out two issues that were fixed:
**Field access control data bug**: Added a new e2e test that was
previously failing - when saving a document, the data object passed to
field-level update access control functions had the wrong shape (or in
some cases did not exist at all). This caused fields to show incorrect
`readOnly` states after save. The new implementation properly passes
document data through all access control checks.
**Where query validation**: The old implementation sometimes skipped
validating `Where` queries for collections when `req.data` existed,
potentially granting incorrect access. The new version always validates
where queries correctly when `fetchData: true`.
**Global Where queries:**: Where queries were _never_ executed for
globals. Instead, [we were just checking if the global existed in
general](https://github.com/payloadcms/payload/blob/main/packages/payload/src/utilities/getEntityPolicies.ts#L59).
This PR ensures that `Where` queries returned from global access control
are respected.
## What's Faster (and Why)
**Where query caching:** When multiple operations return identical where
queries (common pattern), we now cache the result. Instead of 3-4
separate DB calls, we make 1 call and reuse it.
**Parallel execution:** All `Where` query evaluations and async access
control functions (for both collections and fields) now execute
concurrently with maximum parallelism, rather than sequentially.
**Optimized DB operations**: When evaluating returned `Where` queries,
we now use direct database `db.count()` queries instead of
`payload.find()` operations. This is much faster (especially on
Postgres).
**Synchronous tree traversal**: The entire field permission tree is now
built synchronously with all async work collected and executed in
parallel at the end, rather than cascading await calls through each
nesting level. This eliminates sequential bottlenecks in deeply nested
field structures.
## Benchmarks
### Admin Panel
- Fields test suite with access control added
- 1000 blocks added to the blocks collection. This is not as excessive
as you would expect:
- most Payload apps do not run on an M3 Max. CPU/DB is often much, much
slower
- this is all local, with a local DB
- a lot of projects accumulate a huge amount of blocks if you multiply
blocks x block fields. They can definitely reach 1000 total blocks
- each block is simple - only one text field per block. In real
projects, blocks are usually a lot more complex
**Before:**
https://github.com/user-attachments/assets/37b55f02-6dbc-4005-9da6-d7cd4bfdc925
**After:**
https://github.com/user-attachments/assets/8dd796dd-66ed-4d32-a688-27b4169577c6
Branch:
https://github.com/payloadcms/payload/tree/fix/update-field-access-control-after-save-benchmarks
### Modified Access-Control Test Suite
`cd test && pnpm payload run access-control/benchmark-permissions.ts`
```md
📊 Benchmark 1: getAccessResults (all collections + globals)
──────────────────────────────────────────────────────────────────────
┌─────────┬───────────────────┬─────────┬───────────────────┬──────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ms) │ Margin │
├─────────┼───────────────────┼─────────┼───────────────────┼──────────┤
│ 0 │ 'NEW (optimized)' │ '16.91' │ '59203.986' │ '±0.52%' │
│ 1 │ 'OLD (previous)' │ '16.07' │ '62323.953' │ '±0.57%' │
└─────────┴───────────────────┴─────────┴───────────────────┴──────────┘
⚡ Speedup: +5.3% 🚀📊 DB Calls per operation (across all collections + globals):
NEW: 0.0 total (0.0 data, 0.0 where)
OLD: 0.0 total (0.0 data, 0.0 where)
📊 Benchmark 2: docAccessOperation (with fetchData)
──────────────────────────────────────────────────────────────────────
Collection: where-cache-same (same where queries)
┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ms) │ Margin │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0 │ 'NEW (with cache)' │ '2272.81' │ '442.319' │ '±0.11%' │
│ 1 │ 'OLD (no cache)' │ '1019.72' │ '983.347' │ '±0.12%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
⚡ Speedup: +122.9% 🚀📊 DB Calls per operation:
NEW: 2.0 total (1.0 data, 1.0 where)
OLD: 4.0 total (1.0 data, 3.0 where)
📊 Benchmark 3: docAccessOperation (with data passed)
──────────────────────────────────────────────────────────────────────
Collection: where-cache-same (same where queries, no DB fetch)
┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ms) │ Margin │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0 │ 'NEW (with cache)' │ '4945.43' │ '203.252' │ '±0.11%' │
│ 1 │ 'OLD (no cache)' │ '1023.75' │ '979.898' │ '±0.14%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
⚡ Speedup: +383.1% 🚀📊 DB Calls per operation:
NEW: 1.0 total (0.0 data, 1.0 where)
OLD: 4.0 total (1.0 data, 3.0 where)
📊 Benchmark 4: docAccessOperation (unique where queries)
──────────────────────────────────────────────────────────────────────
Collection: where-cache-unique (unique where queries per operation)
┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ms) │ Margin │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0 │ 'NEW (parallel)' │ '1693.67' │ '599.091' │ '±0.29%' │
│ 1 │ 'OLD (sequential)' │ '970.37' │ '1036.268' │ '±0.20%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
⚡ Speedup: +74.5% 🚀📊 DB Calls per operation:
NEW: 4.0 total (1.0 data, 3.0 where)
OLD: 4.0 total (1.0 data, 3.0 where)
📊 Benchmark 5: Complex Collection (async access, nested blocks, field access)
──────────────────────────────────────────────────────────────────────
Collection: complex-content (stress test)
┌─────────┬────────────────────┬──────────┬───────────────────┬──────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ms) │ Margin │
├─────────┼────────────────────┼──────────┼───────────────────┼──────────┤
│ 0 │ 'NEW (optimized)' │ '133.17' │ '7683.346' │ '±0.83%' │
│ 1 │ 'OLD (sequential)' │ '72.86' │ '13901.616' │ '±0.82%' │
└─────────┴────────────────────┴──────────┴───────────────────┴──────────┘
⚡ Speedup: +82.8% 🚀📊 DB Calls per operation:
NEW: 2.0 total (1.0 data, 1.0 where)
OLD: 2.0 total (1.0 data, 1.0 where)
📊 Benchmark 6: Sync-Heavy Collection (same where, many sync field access)
──────────────────────────────────────────────────────────────────────
Collection: sync-heavy (where cache + field access)
┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ms) │ Margin │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0 │ 'NEW (with cache)' │ '3607.72' │ '279.559' │ '±0.15%' │
│ 1 │ 'OLD (no cache)' │ '68.87' │ '15136.557' │ '±1.44%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
⚡ Speedup: +5138.5% 🚀📊 DB Calls per operation:
NEW: 1.0 total (0.0 data, 1.0 where)
OLD: 5.0 total (1.0 data, 4.0 where)
══════════════════════════════════════════════════════════════════════
📈 Summary:
══════════════════════════════════════════════════════════════════════
1. getAccessResults: (see above)
2. docAccessOperation (with fetchData): (see above)
3. docAccessOperation (with data passed): (see above)
4. docAccessOperation (unique where): (see above)
5. Complex collection (async + nested): (see above)
6. Sync-heavy (where cache + fields): (see above)
══════════════════════════════════════════════════════════════════════
```
### Fields Test Suite
`cd test && pnpm payload run fields/benchmark-getAccessResults.ts`
```md
📊 Benchmark: getAccessResults (all fields test collections + globals)
──────────────────────────────────────────────────────────────────────
┌─────────┬───────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ms) │ Margin │
├─────────┼───────────────────┼───────────┼───────────────────┼──────────┤
│ 0 │ 'NEW (optimized)' │ '2104.06' │ '478.382' │ '±0.18%' │
│ 1 │ 'OLD (previous)' │ '1208.14' │ '834.974' │ '±0.21%' │
└─────────┴───────────────────┴───────────┴───────────────────┴──────────┘
⚡ Speedup: +74.2% 🚀📊 DB Calls per operation (across all collections + globals):
NEW: 0.0 total (0.0 data, 0.0 where)
OLD: 0.0 total (0.0 data, 0.0 where)
```
### What?
Prevents unrelated MIME type error being thrown during upload when
`useTempFiles: true`.
### Why?
In [v3.64.0](https://github.com/payloadcms/payload/releases/tag/v3.64.0)
we released a change to check the mimetype on the file buffer. However,
when `useTempFiles` is true, no buffer is passed and this check fails.
### How?
Checks for `!useTempFiles` before returning an error for no buffer data.
Reported by client.
### What?
Fixes `autoRefresh` functionality that was causing "Stay logged in"
modal to appear instead of automatically refreshing the token.
### Why?
Two issues were preventing `autoRefresh` from working:
1. `autoRefresh` was not exposed in the client config, causing it to be
`undefined` in the browser
2. The Auth provider's reminder timeout callback was capturing a stale
`autoRefresh` value due to closure timing during initial component
render
### How?
- Added `autoRefresh` to client config creation to expose it to the
client
- Used `useEffectEvent` in Auth provider to ensure the reminder timeout
handler always captures the latest `autoRefresh` value
### Fixes
#14613
---------
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
### What?
Adds the missing `latest?: boolean` property to the `TypeWithVersion<T>`
type definition.
### Why?
The `latest` property is set by database adapters when creating and
updating versions (both collection and global versions), and is included
in the version field schema when drafts are enabled. However, it was
missing from the TypeScript type definition, causing type errors when
consumers try to access this property on version documents.
### How?
Added `latest?: boolean` as an optional property to the
`TypeWithVersion<T>` type. The property is optional because it's only
present when drafts are enabled in the version configuration.
Fixes#14624
### What?
Ensures `getLocalizedPaths` doesn't falsely detect fields with
locale-like names as a localized field.
### Why?
In the function we are determining if the next segment is a locale based
on whether the segment of the field path matches a locale:
```ts
const nextSegmentIsLocale = localizationConfig && localizationConfig.localeCodes.includes(nextSegment)
```
If you have locale `en` and a nested field with `name: 'en'`, the
function will mistake it for a locale key regardless of whether the
field is localized.
### How?
Uses the existing `fieldShouldBeLocalized` function to check whether the
field is localized in addition to the other checks.
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Relationship fields would fallback even if the parent of the request set
`fallbackLocale: false`. This was because of the logic in
sanitizeFallbackLocale which would transform `false` value into `null`,
and when a relationship populate function ran, it would transform the
`null` into the `localization.defaultLocale` value.
Added int test that failed before and passes after to cover.
When updating a document from a document drawer that isn’t the one
associated with a relationship field, the relationship’s label doesn’t
update correctly.
## Reproduction
This issue is easily reproducible when two relationship fields link to
the same document. If you update the document from the drawer of one
field, the other field will not reflect the update. This happens because
each field only listens to the `onSave` event of its own document
drawer.
## Fix
Use the `useDocumentEvents` hook instead of the `onSave` hook.
`useDocumentEvents` listens for updates to all saved documents, ensuring
that all relationship fields stay in sync regardless of which drawer the
update originated from.
**Before:**
https://github.com/user-attachments/assets/eda152fe-3cda-4111-b0f3-9f0d5a48f37b
**After:**
https://github.com/user-attachments/assets/da252d3d-c0a9-43ee-91d8-508e302d323e
When duplicating documents it should duplicate as draft when drafts are
enabled. It is odd behavior to duplicate a published document and have
the copy immediately be published.
Fixes https://github.com/payloadcms/payload/issues/14377
## Non-public errors were returned from delete and update endpoints
Previously, non-public errors (`500` errors, or errors without
`isPublic: true` and `status`) were returned from the delete and update
endpoint. For security reason, this PR excludes the error messages from
the response.
This aligns bulk endpoints with others (e.g., deleteByID) that already
gate error details through `routeError.ts`.
## Loosen up isPublic check when throwing API error
`APIError` now infers `isPublic` from the status code unless explicitly
provided:
- Non-500 (e.g., 4xx): `isPublic` defaults to `true`.
- 500 or missing status: `isPublic` defaults to `false`.
You can still override by passing a boolean `isPublic` explicitly.
Previously, the only way to mark the error as public was:
`throw new APIError('error message', 401, null ,false)`
which feels unnecessary explicit.
Now, you can do this:
`throw new APIError('error message', 401)`
which previously resulted in `isPublic: false` and now results in
`isPublic: true`.
## Stricter error check in routeError
`routeError` now uses `isErrorPublic(err, config)` to decide whether to
expose the error message. Compared to the old condition (`!config.debug
&& !err.isPublic && status === 500)`, this is a bit stricter and easier
to understand. Errors with `isPublic === false` are now hidden even for
non-500 statuses (previously they were shown).
Adds support for 'UTC' as a value for timezones. This was not previously
supported because we run a check on the values used against the runtime
of Intl API.
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
timezones: {
supportedTimezones: [
{
label: 'UTC',
value: 'UTC',
},
// ...other timezones
],
defaultTimezone: 'UTC',
},
},
})
```
Also fixes an issue when having only one timezone making it selected by
default.
### What?
Updates the `previousValue` data sent to the `afterChange` hook to
accomodate nested paths.
### Why?
Currently the `afterChange` hook returns this:
```ts
previousValue: previousDoc?.[field.name],
```
For any nested fields, this breaks because it does not match the
structure of the incoming data.
### How?
Uses the path segments to correctly extract the relevant data.
**Reported by client.**
### What?
Default values were incorrectly appearing (or not appearing) depending
on access control configuration. When access control denied access to a
global or collection document, default values would sometimes still
populate, and conversely, when access was allowed, defaults would
sometimes be blocked.
### Why?
The previous logic checked `args.data` before querying the database,
making it impossible to distinguish between:
1. A document that doesn't exist and access is allowed (should populate
defaults for globals)
2. A document that was filtered by access control (should NOT populate
defaults)
This caused default values like `_status: 'draft'` to appear when access
control should have returned an empty response, and also blocked
legitimate defaults on uncreated globals without access restrictions.
### How?
- **Query first**: Both globals and collections now query the database
before checking `args.data`
- **Detect access denial**: Only return early when `!docFromDB &&
!args.data && !overrideAccess && accessResult !== true`
- **Correct fallback**: Use `args.data ?? docFromDB ?? {}` (globals) or
`args.data ?? docFromDB` (collections)
- **Simplified**: Removed `shouldSkipDefaults` logic from `promise.ts`
since it's now handled at the operation level
Fixes#14493
Fixes https://github.com/payloadcms/payload/issues/14476
### What?
Adds a basic secondary PDF validation step that runs **after**
`fileTypeFromBuffer`.
### Why?
`fileTypeFromBuffer` is our final mime type check, and currently it will
incorrectly identify any file that starts with `%PDF-1.x` as a PDF. We
are providing some additional **basic** PDF validation steps.
We have opened an
[issue](https://github.com/sindresorhus/file-type/issues/780) on the
[file-type repo](https://github.com/sindresorhus/file-type) to see if
PDF detection can be improved. Update: they said the package is only for
a hint detection, they won't support full PDF validation.
### How?
Introduces a minimal PDF validation function that inspects the decoded
file buffer for required structural elements:
- Ensures the file begins with a valid `%PDF-1.` header
- Confirms the presence of the required `EOF` marker and `xref`
### Considerations
These additional steps will help filter out invalid PDFs but can still
be intentionally manipulated.
Adding a separate PDF validation library might be necessary if this is
something we choose to support.
We can revisit it in the future if stronger PDF validation becomes
necessary.
Fixes [14434](https://github.com/payloadcms/payload/issues/14434)
Fixes https://github.com/payloadcms/payload/issues/14459
We were previously not throwing an error from the endpoint if validation
failed for unlock and the UI was using permissions from the wrong hook,
I've added a jsdoc to explain the difference between permissions form
useAuth and useDocumentInfo
- Adds `overrideAuth` to the MCP Plugin.
- Adds `MCPAccessSettings` as an exported type from the mcpPlugin
```ts
import { type MCPAccessSettings, mcpPlugin } from '@payloadcms/plugin-mcp'
// ... other config
plugins: [
mcpPlugin({
// ... other plugin config
overrideAuth: (req) => {
const { payload } = req
payload.logger.info('[Override MCP auth]:')
return {
posts: {
find: true,
},
products: {
find: true,
update: true,
},
} as MCPAccessSettings
},
})
]
```
### Custom Auth Behaviors
The bypassed system uses an API Key that contains `MCPAccessSettings`
information. The `overrideAuth` function will bypass the API Key system
and use your function when authorizing requests.
This means that your function must return a valid `MCPAccessSettings`
object to use.
Previously, if you called jobs.handleSchedules() with no schedules
defined, the function would throw an error, as the jobs stats global is
not registered when there are no schedules defined.
Now, instead of erroring, the handleSchedules function will return early
with empty arrays for `errored`, `queued` and `skipped`.
Plugin-defined label functions (e.g., `label: ({ t }) =>
t('plugin-redirects:internalLink')`) don't update when switching
languages.
## Root Cause
The global Payload config was being **mutated**. When
`createClientField` copied the `options` array by reference, it would
evaluate label functions and replace them with strings in the global
config. Subsequent requests would find strings instead of functions and
couldn't re-evaluate.
## Why PR #11725 Wasn't Enough
[PR #11725](https://github.com/payloadcms/payload/pull/11725) fixed the
client config cache to store separate configs per language, but didn't
prevent the source config from being mutated.
## Solution
**File**: `packages/payload/src/fields/config/client.ts`
- Skip copying `options` in the default case
- Create a fresh array in the radio/select handler instead of reusing
the reference
- This prevents mutating the global config while still evaluating labels
correctly per language
## Testing
The testing is located at
https://github.com/payloadcms/payload/pull/14569, which depends on this
PR. You can see that the new tests there pass because they already
contain this commit.
I originally created it into a different PR because CI was failing and I
wasn't sure if it was due to this change. Now I've left them separate
for traceability and clarity. The other pull request is a feature, this
one is a fix.
## Description
Automatically injects custom translations from `i18n.translations` into
`supportedLanguages` during config sanitization. This allows translation
functions like `({ t }) => t('namespace:key')` to work with user-defined
custom translations, not just the default translations from
`@payloadcms/translations`.
## Problem
Currently, when users define custom translations in their config:
```typescript
i18n: {
translations: {
de: {
'my-namespace': {
customKey: 'Wert'
}
}
}
}
```
These translations are **not** accessible to the `t` function used in
field labels and options. The `t` function only searches in
`config.i18n.supportedLanguages[lang].translations`, causing custom
translations to be ignored.
This issue was discovered while implementing translation support for
plugins in PR #14548, where radio button options using `label: ({ t })
=> t('plugin-redirects:key')` failed to translate for custom languages.
## Solution
During config sanitization (in
`packages/payload/src/config/sanitize.ts`), the code now:
1. Iterates through all languages in `i18n.translations`
2. For existing languages in `supportedLanguages`: merges custom
translations with existing ones
3. For new languages: creates them using English as a template, then
merges custom translations
This ensures that **all custom translations** defined by users or
plugins are automatically available to the `t` function throughout the
application.
### What?
In field hooks, `previousSiblingDoc` is typed as `TData`.
### Why?
This value should actually be typed as `TSiblingData`, I've confirmed
the sibling data is actually what is returned when a field hook runs.
### How?
Updated `FieldHook` typing, as well as `RichText` field hook typings
Fixes#9735
The way we parse query params within endpoint handlers is currently
non-standard. This leads to subtle differences in how they are handled
across endpoints, e.g. using searchParams vs req.query, allowing values
to fallback to undefined, etc.
For background, each endpoint accepts arbitrary query params and must
transform them into what Payload expects.
For example:
- `draft` provided as a string of "true" is converted to a boolean
- `depth` provided as a string of "0" is converted to a number
- `sort` provided as a comma-separated string is converted to an array
of strings
It is also easy to overlook params that should fallback to `undefined`.
Not doing so will prevent the underlying operation from using its
defaults, if any:
```ts
await update({
// ...
trash: trash === 'true' // This is never `undefined` so the default arg will not used (if any)
})
```
Parsing these params is now shared via a single `parseParams` utility,
which takes in raw query parameters and returns them typed and properly
formatted.
```ts
const {
depth, // number | undefined
draft // number | undefined
} = parseParams(req.query)
```
This PR adds support for polymorphic uploads similar to polymorphic
relationships. Closes
https://github.com/payloadcms/payload/discussions/13912
It works with bulk upload as well and in the bulk uploader you can
choose which collection the files are uploaded into.
You can enable this by adding an array of slugs into `relationTo`:
```ts
import type { CollectionConfig } from 'payload'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'media',
type: 'upload',
relationTo: ['images', 'documents', 'videos'], // references multiple upload collections
},
],
}
```
It can also be combined with `hasMany`:
```ts
import type { CollectionConfig } from 'payload'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'media',
type: 'upload',
relationTo: ['images', 'documents', 'videos'], // references multiple upload collections
hasMany: true, // allows multiple uploads
},
],
}
```
**Known issue**: https://github.com/payloadcms/payload/issues/12014
Filtering by polymorphic relationships is currently broken and I'll work
on that in a separate PR.
When getting values from the KV store, it is currently not possible to
type the return value without using assertions, etc. This is because
`payload.kv.get()` does not support generic types, forcing you to use a
workaround like this:
```ts
const value = await payload.kv.get('my-key') as MyValue
// or
const value: MyValue = await payload.kv.get('my-key')
// and so on...
```
Now, you can use generics and the return type will be applied
automatically:
```ts
const value = await payload.kv.get<MyValue>('my-key') // `value` is now typed as `MyValue`
```
### What?
Fixes pagination when grouping by virtual relationship fields in
collection list views. The sticky pagination toolbar now correctly
appears when there are 10+ distinct values, and `totalPages` is
calculated accurately.
### Why?
When grouping by virtual relationship fields (e.g.,
`virtualTitleFromCategory` with `virtual: 'category.title'`), the
pagination was broken:
- `totalPages` always returned `1` even when there were 10+ distinct
values
- The sticky pagination toolbar at the bottom never appeared
This happened because:
1. The recursive `findDistinct` call for virtual fields wasn't passing
through `limit` and `page` parameters
2. MongoDB's count aggregation pipeline was missing the relationship
`$lookup` stages needed to traverse virtual field paths
### How?
**1. Fixed recursive findDistinct call**
- Added `limit` and `page` parameters to the recursive call when
handling virtual fields
**2. Fixed MongoDB count aggregation**
- Changed count pipeline to include the same stages as the main query
(`sortAggregation`, `relationLookup`, `$unwind`, `$group`)
- Ensures relationship lookups are performed before counting distinct
values
## Changes
This PR consolidates the inline interface name generation logic in
`configToJSONSchema.ts` to use the existing `formatNames()` utility
function.
## Motivation
The inline code `singular(toWords(slug, true))` was duplicating logic
already available through `formatNames(slug).singular`. Using the
existing utility function:
- Reduces code duplication
- Maintains consistency with the rest of the codebase
- Makes it clear where the interface naming logic lives
## Testing
Verified that `formatNames(slug).singular` produces identical results to
the previous inline implementation across various slug patterns (plural,
singular, edge cases like 'media', 'series', etc.).
**No behavioral changes** - this generates the exact same interface
names for all collection and global slugs.
Fixes https://github.com/payloadcms/payload/issues/14419
## Problem
Next.js 16 changes the order of `getPayload()` calls during HMR. In
testing, the frontend now calls `getPayload()` before the admin panel
does. Since the frontend typically has `skipImportMapGeneration: true`
(due to no importMap passed to `getPayload`), this was skipping import
map generation entirely, breaking the admin panel.
## Solution
This PR ensures the import map is always regenerated during HMR by
passing `false` as the second parameter to `reload()`, regardless of the
`options.importMap` value passed to `getPayload()`.
Additionally, a new `ignoreResolveError` option is added to handle cases
where `getPayload()` is called exclusively from the frontend (where no
import map file exists). This prevents errors from interrupting the HMR
process in frontend-only scenarios.
Next.js 16 broke HMR, as the shape of the data sent by the hmr websocket
has changed.
Next.js 15: `{ action: 'serverComponentChanges', hash: '11' }`
Next.js 16: `{ type: 'serverComponentChanges', hash: '16' }`
This PR adds back HMR support by checking for both `action` and `type`.