Commit Graph

1997 Commits

Author SHA1 Message Date
German Jablonski ad8f0b2c1b feat(plugin-redirects): support translations (#14548)
Allows you to add translations to the plugin fields, replacing the need
for verbose field `overrides` configuration.

### Usage

```ts
export default buildConfig({
  i18n: {
    translations: {
      en: {
        'plugin-redirects': {
          fromUrl: 'Source URL (Custom)', // Override
        },
      },
      de: {
        'plugin-redirects': {
          fromUrl: 'Quell-URL',
          internalLink: 'Interner Link',
          // ... other keys
        },
      },
    },
  },
  plugins: [redirectsPlugin({ collections: ['pages'] })],
})
```
2025-11-13 15:22:51 +00:00
Jarrod Flesch 99ed0e526a fix: findGlobalVersionByID id type (#14534)
Adjusts the `id` type on `findGlobalVersionByID` to be `number |
string`, previously was `string`.
2025-11-13 10:00:18 -05:00
Jessica Rynkar ce479abddc fix: add secondary PDF mimeType validation and tests (#14529)
### 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)
2025-11-13 14:49:22 +00:00
Paul f63e34e686 fix: unlock access not being correctly denied when using a where query (#14585)
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
2025-11-13 09:15:11 -05:00
Kendell 73a18dc088 feat(plugin-mcp): adds custom auth config (#14538)
- 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.
2025-11-12 14:21:43 -05:00
Alessio Gravili 072cb97ea1 fix: error when calling jobs.handleSchedules when there are no schedules defined (#14519)
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`.
2025-11-12 12:45:27 -06:00
German Jablonski bb520d6e78 fix: changing the language of the admin panel does not work for "radio" and "select" fields. (#14569)
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.
2025-11-12 16:42:50 +00:00
German Jablonski d7f1ea2ba9 fix: inject custom translations into supportedLanguages in sanitization (#14549)
## 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.
2025-11-10 17:04:34 +00:00
Slava Nossar 339a0c390d fix: use TSiblingData for previousSiblingDoc in FieldHook (#14503)
### 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
2025-11-06 20:59:07 +00:00
Jacob Fletcher 3c9f56eb41 chore: standardize query param parsing (#14498)
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)
```
2025-11-06 19:02:13 +00:00
Paul df40d0e6fe feat: add support for polymorphic uploads (#14363)
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.
2025-11-06 14:40:51 +00:00
Jacob Fletcher f6194c6c91 fix: support generic types for kv.get (#14494)
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`
```
2025-11-05 17:39:03 +00:00
Patrik 648021c13b fix: group-by sticky pagination not rendering for virtual relationship fields (#14470)
### 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
2025-11-05 08:18:02 -08:00
Tobias Odendahl 39bead7e4c refactor: use formatNames utility for interface name generation (#14481)
## 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.
2025-11-05 09:58:37 +00:00
Alessio Gravili 61a7095af3 fix: ensure importMap is reliably generated during HMR (fixes Next.js 16 issue) (#14474)
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.
2025-11-05 02:04:18 +00:00
Alessio Gravili 0231a8d1f9 fix: hmr not working in next.js 16 (#14462)
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`.
2025-11-04 03:34:23 +00:00
Sasha 827c9a16ad fix: disable lockedDocuments if the kv adapter uses a collection (#14453)
Fixes https://github.com/payloadcms/payload/discussions/14452

* Adds `collection.lockDocuments: false` to the KV storage collection if
the adapter has one.
* Filters `relationTo` for the locked documents collection with removing
collections that have `lockDocuments: false` in their config.
2025-11-03 15:44:07 +00:00
Jessica Rynkar 53d85574d8 feat: adds new UI option Duplicate In [Select Locales] (#13803)
### What
Adds new UI feature and update to the duplicate endpoint to support to
duplicating only select locales.

### Why
Currently, duplicating a document always copies all locales. To exclude
some, users must duplicate the full document and manually clear fields.
This feature was requested by the community and is part of planned
localization improvements.

### How
The new control opens a drawer where users can pick one or more locales
to duplicate. The duplicate endpoint now accepts `selectLocales` and
automatically clears any unselected locales before creating the new
draft document.

#### Feature tracked here:
https://github.com/payloadcms/payload/discussions/13444

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-10-31 16:06:36 -04:00
Elliot DeNolf 64209271ea build: add kv-redis to publishList (#14416) 2025-10-30 14:13:18 -04:00
Dan Ribbens 10a640cbf4 fix(db-mongodb): migration fails for cosmosDB (#14401)
fixes #10136
2025-10-30 13:50:33 -04:00
Alessio Gravili 0a37edbc97 feat: add access control for jobs queue and cancel operations (#14404)
Adds access control for jobs `queue` and `cancel` operations, similar to
what we have for `run` operations.

**Implementation:**
Both operations now support `overrideAccess` parameter (defaults to
`true`) and respect `jobsConfig.access.queue` and
`jobsConfig.access.cancel` functions. When `overrideAccess: false` is
passed without an authenticated request, operations throw `Forbidden`
error.

**Configuration:**
Access control functions can be defined in the Jobs Config under
`jobs.access.queue`, `jobs.access.run`, and `jobs.access.cancel`. Each
function receives `{ req }` and returns a boolean. If no custom access
control is defined, the default allows any authenticated user to perform
the operation.

**Example:**
```ts
// Configure access control
jobs: {
  access: {
    queue: ({ req }) => req.user?.roles?.includes('admin'),
    cancel: ({ req }) => req.user?.roles?.includes('admin'),
  }
}

// Use in Local API
await payload.jobs.cancel({
  where: { workflowSlug: { equals: 'sync' } },
  overrideAccess: false,
  req,
})
```
2025-10-30 16:27:24 +00:00
Paul a3df837872 feat: timezone config can now be added on a per field basis (#14410)
This PR enables you to add timezone specific config on a per field basis
so each field can support its own list of timezones

```ts
{
  name: 'date',
  type: 'date',
  timezone: {
    defaultTimezone: 'America/New_York',
    supportedTimezones: [
      { label: 'New York', value: 'America/New_York' },
      { label: 'Los Angeles', value: 'America/Los_Angeles' },
      { label: 'London', value: 'Europe/London' },
    ],
  },
}
```

You can also enforce a specific timezone by specifying just one with a
default value:

```ts
{
  name: 'date',
  type: 'date',
  timezone: {
    defaultTimezone: 'Europe/London',
    supportedTimezones: [
      { label: 'London', value: 'Europe/London' },
    ],
  },
}
```

It also fixes a bug with date fields in list view not showing the date
in the formatted timezone correctly.
2025-10-30 11:12:37 -04:00
Sasha ffb9a2e225 feat: add KV storage adapters (#9913)
Adds KV storage support to Payload Local API with an adapter pattern,
includes 3 adapters. We'll use it for the Realtime API.
You can access it via `payload.kv`:

```ts
interface KVAdapter {
  /**
   * Clears all entries in the store.
   * @returns A promise that resolves once the store is cleared.
   */
  clear(): Promise<void>

  /**
   * Deletes a value from the store by its key.
   * @param key - The key to delete.
   * @returns A promise that resolves once the key is deleted.
   */
  delete(key: string): Promise<void>

  /**
   * Retrieves a value from the store by its key.
   * @param key - The key to look up.
   * @returns A promise that resolves to the value, or `null` if not found.
   */
  get(key: string): Promise<KVStoreValue | null>

  /**
   * Checks if a key exists in the store.
   * @param key - The key to check.
   * @returns A promise that resolves to `true` if the key exists, otherwise `false`.
   */
  has(key: string): Promise<boolean>

  /**
   * Retrieves all the keys in the store.
   * @returns A promise that resolves to an array of keys.
   */
  keys(): Promise<string[]>

  /**
   * Sets a value in the store with the given key.
   * @param key - The key to associate with the value.
   * @param value - The value to store.
   * @returns A promise that resolves once the value is stored.
   */
  set(key: string, value: KVStoreValue): Promise<void>
}
```

To configure the adapter you can use the `kv` property of the root
config.
```ts
buildConfig({
  kv: adapter()
})
```

#### Database KV adapter (default)
No need to configure, as Payload uses it by default. It generates new
collection `payload-kv` and uses the current database adapter to access
it. The collection is hidden in the admin panel and access locked.
If you want to  override the generated collection:
```ts
import { databaseKVAdapter } from 'payload'

buildConfig({
  kv: databaseKVAdapter({
    kvCollectionOverrides: {
      slug: 'custom-kv',
      ...(process.env.DEBUG === 'true' && {
        admin: { hidden: false },
        access: {},
      }),
    },
  }),
})
```

#### In Memory KV adapter
Simple and very fast storage using memory. Don't use it on Vercel /
multiple instances or if you need data persistence.
```ts
import { inMemoryKVAdapter } from 'payload'

buildConfig({
  kv: inMemoryKVAdapter(),
})
```

#### Redis KV Adapter
Uses Redis. Probably the best option as it's faster than database,
persistent and works with Vercel / multiple instances, but requires
additional setup
```sh
pnpm add @payloadcms/kv-redis
```

```ts
import { redisKVAdapter } from '@payloadcms/kv-redis'

buildConfig({
  kv: redisKVAdapter({
    keyPrefix: "custom-prefix:", // defaults to 'payload-kv:'
    redisURL: "redis://127.0.0.1:6379" // defaults to process.env.REDIS_URL (Vercel generates this variable for you if you connect a project to Redis)
  }),
})
```
2025-10-30 17:05:38 +02:00
Patrik b766ae6570 fix: globals with versions return _status field when access denied (#14406)
### What?

Fixes an issue where globals with drafts/versions enabled would return
the `_status` field with its default value (`'draft'`) when access
control denied read access, instead of returning an empty object.

### Why?

When a global has:
1. Versions/drafts enabled (which adds the `_status` field with
`defaultValue: 'draft'`)
2. Read access control with a where clause (e.g., `return {_status:
{equals: 'published'}}`)

And a user without permissions requests the global (e.g., an unpublished
draft exists), the API was returning `{"_status": "draft"}` instead of
an empty object `{}`. This exposes the document's status even when
access is denied.

The issue occurred because when the DB query returned `null` (filtered
by access control), the `findOne` operation would set `doc = {}`, and
then the `afterRead` hook would populate default values including
`_status: 'draft'`.

### How?

- Modified `afterRead/promise.ts` to skip setting default values for
globals when:
- The document has no `id` (indicating it doesn't exist or was filtered)
  - AND access control is active (`!overrideAccess`)

Fixes #14096
2025-10-30 07:57:56 -07:00
Philipp Schneider d5fab43fe9 feat: make toast position configurable (#14405)
### What?

Extends the toast configuration introduced with #13609 to also allow to
configure the toast's positioning.

### Why?

Different users of Payload might prefer different positions. E.g. for
our own implementation we'd prefer a top (central) position, because
we've been having users overlook the toasts in the bottom right due to
usage of large screens, and saving / publishing buttons usually
triggering toasts being placed at the top (meaning the user looks at the
top).

### How?

The `position` configuration is a 1:1 pass-through of the
https://sonner.emilkowal.ski `position` configuration option. The
default remains unchanged compared to the status-quo, at
`'bottom-right'`.

### Screenshots

*`bottom-right`:* Unchanged to current placement.

*`bottom-center`:*

<img width="1283" height="1373" alt="image"
src="https://github.com/user-attachments/assets/ad2716d4-3a8b-467e-be03-80abf0e1233f"
/>

*`bottom-left`:*

<img width="1283" height="1375" alt="image"
src="https://github.com/user-attachments/assets/4f7b1efe-de1a-4a48-be5d-9746ba4e7296"
/>


*`top-left` - questionable, but IMHO that's fine because it's an opt-in
choice.*:

<img width="1276" height="1371" alt="image"
src="https://github.com/user-attachments/assets/ff11e2f5-0aa0-411a-9bcf-01083184a531"
/>

*`top-center`:*

<img width="1285" height="1384" alt="image"
src="https://github.com/user-attachments/assets/603e735f-36f5-4b5a-8e9f-b1c80b2a2907"
/>

*`top-left` - also covers interactive UI, but is therefore very hard to
overlook:*

<img width="1283" height="1370" alt="image"
src="https://github.com/user-attachments/assets/40ecdd57-a9d2-4d47-b8d9-26e64164e1ee"
/>
2025-10-29 23:45:32 +00:00
Alessio Gravili 1341f69e07 refactor: deprecate job queue depth property (#14402)
Similar to how we deprecated the `runHooks` property in
https://github.com/payloadcms/payload/pull/13617, this PR deprecates the
`depth` property.

Just like `runHooks`, setting depth to 1 or higher will massively slow
down the job system and make it less reliable. It's also adds a lot of
unnecessary code we will need to maintain.

To further discourage using this property, this PR marks it as
deprecated and for removal in 4.0.
2025-10-29 17:42:46 +00:00
Sasha 059185f88f fix(db-*): findMigrationDir in projects without src folder (#14381)
Fixes https://github.com/payloadcms/payload/issues/14379

Additionally, removes duplication of `findMigrationDir` between DB
adapters, deprecates the export from `@payloadcms/drizzle` and adds unit
testing to this function.
2025-10-28 18:57:26 +00:00
Jarrod Flesch 5a37909134 fix: filteredLocales in the client config are stale (#14326)
Fixes https://github.com/payloadcms/payload/issues/14299

Using `filterAvailableLocales` means they could change when the req
changes. When we get the client config from the cache, we need to
re-apply `filterAvailableLocales` so the config has the correct values.

This was manually being done in the Root layout, but every other place
that called getClientConfig was not i.e. the Root view. So this PR moves
that logic from the Root layout into the getClientConfig function.
2025-10-24 21:11:54 +00:00
Paul ad0e7b2252 fix(ui): preview button not responding to conditional URL (#14277)
Fixes #14241 and #11253.

Preview URL is now part of the form state as well so we can
conditionally show the preview button based on its value, previously
this was not possible because we were fetching the URL on click. Similar
to #14012.
2025-10-23 13:35:22 -04:00
Elliot DeNolf 894cf0c034 build: add plugin-mcp to publishList (#14319)
Add `@payloadcms/plugin-mcp` to publish list.
2025-10-23 10:51:17 -04:00
Patrik a1671ec0f5 fix: user updatedAt modified during session operations (#14269)
### What?

The `updatedAt` field on user documents was being updated whenever
session operations occurred (login, logout, or token refresh), even
though only session metadata was changing and not the actual user data
itself.

### Why?

This behavior was introduced in PR #13335, which added explicit
`updatedAt = new Date().toISOString()` to all session operations to
ensure that `updatedAt` would always be bumped when using
`payload.db.updateOne()`.

However, this was incorrect for session operations specifically. The
`updatedAt` timestamp should only change when the user's actual data
(email, password, profile fields, etc.) is modified - not when internal
session tracking metadata changes. Session management is essentially
bookkeeping and shouldn't trigger timestamp updates that suggest the
user entity itself was modified.

### How?

Modified session operations to set `updatedAt = null` instead of
updating it:
- `addSessionToUser()` in `packages/payload/src/auth/sessions.ts`
- `logoutOperation()` in
`packages/payload/src/auth/operations/logout.ts`
- `refreshOperation()` in
`packages/payload/src/auth/operations/refresh.ts`

Setting `updatedAt = null` skips updating this field entirely, leaving
the existing timestamp unchanged.

Fixes #13626
2025-10-22 12:21:34 -07:00
Jarrod Flesch c2dcd122e1 chore: extract snapshot logic, correct types in version ops (#14290)
Adds a `createSnapshot` to improve readability in the save versions
file.

Corrects update/create version types for collections and globals.
2025-10-22 13:21:35 -04:00
Paul b252658533 chore: bump pino to v9.14.0 and pino-pretty (#14283)
Continuing from https://github.com/payloadcms/payload/pull/14262

Bumping `pino` to `9.14.0` and `pino-pretty` to `10.1.2`, both minor
version bumps
2025-10-22 15:33:22 +00:00
Patrik 1016cd0cd4 fix: typescript requires fields when draft: true despite passing draft: true (#14271)
### What?

Fixes TypeScript types for `payload.create()` to allow omitting required
fields when `draft: true` is specified, aligning type checking with
runtime validation behavior.

### Why?

According to the
[documentation](https://payloadcms.com/docs/versions/drafts#updating-or-creating-drafts),
when creating drafts, required fields don't need to be provided because
validation is skipped by default (unless `versions.drafts.validate` is
explicitly set to `true`). However, TypeScript was still enforcing
required fields at the type level, causing type errors even when `draft:
true` was set.

### How?

- Added new types `DraftDataFromCollectionSlug` and
`DraftDataFromCollection` that make all fields optional
- Changed `payload.create` options to use a discriminated union type:
- When `draft: true` → uses `DraftDataFromCollectionSlug` (all fields
optional)
- When `draft: false` or omitted → uses `RequiredDataFromCollectionSlug`
(required fields enforced)

The fix uses TypeScript's discriminated unions to provide accurate type
inference based on the `draft` parameter value.

Fixes #12578
2025-10-21 06:26:58 -07:00
Alessio Gravili d57be12cf9 chore: cleanup, short-circuit .split calls (#14245)
Just some small optimizations, extracted from
https://github.com/payloadcms/payload/pull/14244 to make the diff easier
to read:

- short circuit .split() calls => faster
- remove unused code and clean up code
2025-10-17 23:50:38 +00:00
Alessio Gravili 8b0ac01f41 docs: add jsdocs to RichText adapter (#14246)
Adds JSDocs to the RichText adapter return value.
2025-10-18 02:46:58 +03:00
Paul 88cb687733 fix: blocks access control not respecting update access whether on collection or on a per field basis (#14226)
Fixes https://github.com/payloadcms/payload/issues/14221
Fixes https://github.com/payloadcms/payload/issues/13569

Fixes a problem where blocks would have their fields disappear entirely
if update permissions were false on the collection or on the blocks
field itself.

This fixes that and makes sure that fields are always visible and that
the blocks fields are correctly being set to readOnly mode if you have
permissions to read but not update
2025-10-17 23:11:42 +01:00
Paul 2ced43dc56 fix: allow slugField to accept localized argument and fixed slug generation with custom field names (#14234)
Previously you had to use the overrides to enable localization for the
`slugField` however it's a very common request that slugs are localized
so we've added it to the args.

Now you can do:
```ts
slugField({ localized: true })
```

There was also a bug relating to using custom field names, even though
we accept it the hook would still look for `slug`, this has been fixed.

Todo:
- [x] Add e2e tests for general field and localized field
2025-10-17 17:07:16 +00:00
Alessio Gravili 54224c3ab7 perf(richtext-lexical): do not return i18n from editor adapter (#14228)
This PR deprecates the `i18n` property returned from the editor adapter.
Instead, we now merge `i18n` directly into the `config`, since the
config is already available in the richtext adapter provider. This
simplifies the logic (only one line of code) and eliminates the need to
manually read and merge i18n from the adapter.

Benefits:
- **Simpler implementation**: Merging i18n into the config is
straightforward
- **Reduced memory usage**: Each editor no longer maintains its own i18n
object - only a single, merged `config.i18n` is kept.
- **Improved developer experience**: The sanitized field config is now
smaller and easier to inspect. Console logging the config no longer
floods the output with hundreds of translation lines.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211668727538425
2025-10-17 09:19:42 +01:00
Sasha 2b1b6eee65 fix: findDistinct by explicit ID paths in relationships and virtual fields (#14215)
Fixes `findDistinct` operation when the `field` argument is:
* an eplicit ID path inside a relationship like:
```ts
const distinct = await payload.findDistinct({ collection: 'posts', field: 'category.id' })
```
* A virtual field linked to a relationship ID field
```ts
[
  {
    type: 'relationship',
    relationTo: 'categories',
    name: 'category',
  },
  {
    type: 'number',
    name: 'categoryID',
    virtual: 'category.id',
  },
]

const distinct = await payload.findDistinct({ collection: 'posts', field: 'categoryID' })
```
2025-10-16 18:45:00 +03:00
Jessica Rynkar 623a1b8216 feat: accept multiple locales in fallbackLocale (#13822)
### What?
This change allows `fallbackLocale` to accept an **array of locales** in
find queries and locale configuration.

```ts
/** Local API **/
  await payload.findByID({
    id,
    collection,
    locale: 'en',
    fallbackLocale: ['fr', 'es'],
  })

/** REST API **/
  await fetch(`${baseURL}/api/${collectionSlug}?locale=en&fallbackLocale[]=fr&fallbackLocale[]=es`)

/** GraphQL **/
  await restClient.GRAPHQL_POST({
    body,
    query: { locale: 'en', fallbackLocale: ['fr', 'es']},
  })

/** Locale Configs **/
  locales: [
    {
      code: 'en',
      label: 'English',
      fallbackLocale: ['fr', 'es'],
    },
  ]
```

### Why?
This update is part of the planned [localization
enhancements](https://github.com/payloadcms/payload/discussions/10705).
Currently, only one fallback locale can be specified. If that locale
doesn’t contain data, the query returns nothing. To work around this,
users have to inspect the response themselves and then make additional
queries for other locales.

With this change, Payload handles that work automatically. Users only
need to provide a list of fallback locales in their preferred order, and
Payload will check each one until it finds a value.

### How?
Updates query handling across the local API, REST API, and GraphQL so
that `fallbackLocale` can accept either a **string** or an **array** of
locales. When an array is passed, Payload will iterate through them in
order and return the first found localized value. This behavior applies
to both collections and globals.

#### Feature request:
https://github.com/payloadcms/payload/discussions/13443

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-10-16 12:02:59 +01:00
German Jablonski 13a1d90890 fix: validate Point Field to -180 to 180 for longitude and -90 to 90 for latitude (#14206)
Fixes #14185
2025-10-16 07:47:25 +00:00
Patrik ee8b3cff96 feat: adds settingsMenu to admin navigation sidebar (#14139)
### What?

Adds a new `admin.components.settingsMenu` configuration option that
allows users to inject custom menu items into the navigation. When
configured, a gear icon appears above the logout button in the
navigation sidebar. Clicking the gear icon opens a popup menu displaying
the custom components.

<img width="130" height="177" alt="Screenshot 2025-10-14 at 11 43 18 AM"
src="https://github.com/user-attachments/assets/5583e32d-cde3-45f0-9d58-a6724c12509f"
/>

<img width="174" height="239" alt="Screenshot 2025-10-14 at 11 43 37 AM"
src="https://github.com/user-attachments/assets/5dca8ea7-ed99-4bc1-89c2-bc6da7d8f323"
/>

### Why?

Users need a way to add custom actions and menu items to the admin
navigation without replacing the entire navigation component. This
provides a standardized location for admin-level utilities and actions
that don't fit into collection or global navigation.

### How?

- Added `settingsMenu` property to `admin.components` config type
definition
- Created `GearIcon` component following the existing icon patterns
- Exported `GearIcon` from the UI package exports
- Implemented `SettingsMenuButton` client component that renders a popup
menu with the gear icon trigger
- Integrated `SettingsMenuButton` into `DefaultNav` component,
positioned above the logout button
- Updated import map generation to include `settingsMenu` components
2025-10-15 11:21:29 -07:00
German Jablonski 747a049753 fix: urls in upload sizes are not encoded (#14205)
Fixes #14195
2025-10-15 18:02:03 +01:00
Patrik 3317207d57 fix: restoring trashed drafts with empty required fields fails validation (#14186)
### What?

This PR fixes the validation logic for restoring documents from trash.
Previously, restoring a trashed draft with empty required fields would
fail with "The following fields are invalid" errors, even when restoring
as a draft.

### Why?

When a draft document with empty required fields is trashed and then
restored:
- Trashing works correctly (skips validation since it's just setting
`deletedAt`)
- Restoring as draft should also work (it's just metadata, clearing
`deletedAt`)
- Restoring as published should require validation (content is being
published)

The previous fix only handled trashing (when `deletedAt` is set), but
didn't handle the restore case (when `deletedAt` is set to `null`).

### How?

Extended the `skipValidation` logic in `updateDocument()` to detect
restore operations.

Fixes #14180
2025-10-15 06:59:41 -07:00
Alessio Gravili 07a1eff99c feat(richtext-lexical): client-side block markdown shortcuts, code block (#13813)
This PR:
- adds support for client-side markdown shortcuts for blocks with the
`admin.jsx` property. This was previously only supported server-side.
The code mimics the server-side implementation, as this could be used
for full markdown imports on the client-side, rather than just simple
markdown shortcuts
- exports a pre-made `CodeBlock` that can be used within the
`BlocksFeature`. Video Example:


https://github.com/user-attachments/assets/31f2d66b-64e9-4e9d-a2b6-89ddefda3a84



---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211217132290839
2025-10-07 20:15:45 +00:00
Jacob Fletcher b09ae6772f feat: slug field (#14007)
Discussion #8859. Requires #14012.

Exports a new `slugField`. This is a wrapper around the text field that
you can drop into any field schema.

A slug is a unique, indexed, URL-friendly string that identifies a
particular document, often used to construct the URL of a webpage. Slugs
are a fundamental concept for seemingly every project.

Traditionally, you'd build this field from scratch, but there are many
edge cases and nice-to-haves to makes this difficult to maintain by
hand. For example, it needs to automatically generate based on the value
of another field, provide UI to lock and re-generate the slug on-demand,
etc.

Fixes #13938.

When autosave is enabled, the slug is only ever generated once after the
initial create, leading to single character, or incomplete slugs.

For example, it is expected that "My Title" → "my-title, however ends up
as "m".

This PR overhauls the field to feel a lot more natural. Now, we only
generate the slug through:
1. The `create` operation, unless the user has modified the slug
manually
2. The `update` operation, if:
  a. Autosave is _not_ enabled and there is no slug
b. Autosave _is_ enabled, the doc is unpublished, and the user has not
modified the slug manually

The slug should stabilize after all above criteria have been met,
because the URL is typically derived from the slug. This is to protect
modifying potentially live URLs, breaking links, etc. without explicit
intent.

This fix, along with all the other features, is now standardized behind
the new `slugField`:

```ts
import type { CollectionConfig } from 'payload'
import { slugField } from 'payload'

export const MyCollection: CollectionConfig = {
  // ...
  fields: [
   // ...
   slugField()
  ]
}
```

In the future we could also make this field smart enough to auto
increment itself when its generated slug is not unique.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211513433305005
2025-10-07 10:15:45 -04:00
Dan Ribbens 9ceee8ea3c chore: ci changes to add compatibility for mongodb alternates (#13898)
- Adds documentdb and cosmosdb to CI test matrix
- Adds missing generateDatabaseAdapter configs
- Adjust generateDatabaseAdapter firestore config to make it pure to the
compatibilityOptions we provide as an export

Creating as draft because I expect a few tests to fail based on previous
comments.

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2025-10-06 16:48:02 -04:00
Sasha e4f8478e36 feat: support any depth for relationships in findDistinct (#14090)
Follow up to https://github.com/payloadcms/payload/pull/14026

```ts
// Supported before
const relationResult = await payload.findDistinct({ collection: 'posts', field: 'relation1.title' })
// Supported now
const relationResult = await payload.findDistinct({ collection: 'posts', field: 'relation1.relation2.title' })
const relationResult = await payload.findDistinct({ collection: 'posts', field: 'relation1.relation2.relation3.title' })
```
2025-10-06 22:44:14 +03:00
Paul 3cf3f93e1f fix: add detection for --experimental-https flag (#14085)
Follow up from https://github.com/payloadcms/payload/pull/14053

Add detection for `--experimental-https` flag based on `process.argv`
which may not be reliable so we're keeping the env var toggle in as well
2025-10-06 12:06:15 +00:00