### 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`.
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.
### 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>
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,
})
```
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.
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)
}),
})
```
### 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
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.
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.
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.
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.
### 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
### 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
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
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
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
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
### 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>
### 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
### 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
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
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
- 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>
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