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 the D1 Cloudlfare template to the CPA command - if the D1 adapter
is selected then it won't prompt for a DB connection string since this
database doesn't use one
- Exports additional modules from `@payloadcms/next` like
`DashboardView`, which are useful for when you provide your own view but
want to render parts of the default view
- Passes all props to custom dashboard views, giving you access to
things like `req`
- Makes use of `select` API in `payload.find` call of the dashboard view
for improved performance
- 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>
Fixes#14032.
When duplicating arrays that contains nested arrays, the newly
duplicated row will include twice as many fields within the nested array
as the original.
This is because duplicated row ids don't match 1:1 with their parent's
`rows` property.
For example: `array.0.nestedArray.0.id` should match
`array.0.nestedArray.rows[0].id`.
The problem is that when we duplicate the row, we regenerate all nested
id fields, but we fail to sync them its corresponding row in its parent
field. When we go to build form state on the server, we build new fields
for this stale row. Then when we merge server form state back into local
state, those new fields are simply _merged_ with the local state without
replacing them.
This is how we determine whether local changes to a row took place while
a form state request was pending. It is important to prevent the merge
strategy from overriding your active changes.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211543194775558
Closes https://github.com/payloadcms/payload/issues/10378
We now pass `collection` containing the current collection's config into
the `generateURL` and `generateLabel` as a third parameter.
In the future we should change this into an object so we can more easily
insert new params.
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
This commit provides a comprehensive update to the Persian (fa-IR)
translation files, focusing on improving the overall user experience by
adopting a more natural, modern, and consistent tone.
The previous translations, while functional, often used overly formal,
literal, or archaic vocabulary that can feel unnatural in a modern web
application. This revision addresses these issues across all major
sections of the CMS.
Key improvements include:
- **Modern Terminology:** Replaced archaic or formal words with their
common, modern equivalents (e.g., "نگارش" -> "نسخه", "رایانامه" ->
"ایمیل", "پیوند" -> "لینک").
- **Natural Phrasing:** Rephrased verbose confirmation dialogs and
validation messages into concise, direct questions and statements that
are standard in UI/UX design.
- **Correction of Mistranslations:** Fixed incorrect translations for
key terms like `crop`, `locale`, `type` (version), and `character`
length validation.
- **Consistency:** Ensured consistent terminology for common elements
like "field", "file", "version", and "upload" across all sections.
- **Improved Flow:** Adjusted sentence structures to read more naturally
for a native Persian speaker.
Closes https://github.com/payloadcms/payload/issues/12087
When using `--experimental-https` from Nextjs we have no way of
detecting that right now so I've added documentation on how to handle
this flag and added to support for `USE_HTTPS` to set the websocket
protocol for HMR to `wss` instead of `ws`
Optimized database queries generated by graphQL by only selecting the
fields that have been requested. This is particularly useful for
collections with fields of type "join" and "relationship"
Co-authored-by: Ricardo Tavares <rtavares@cloudflare.com>
### What?
Fixes "Invalid time value" error in the DocumentLocked modal when
displaying the last edited timestamp.
### Why?
The `formatDate` function was passing a timestamp number directly to
`Intl.DateTimeFormat().format()`, which expects a Date object. When
`updatedAt` is a number (timestamp), this causes an "Invalid time value"
error.
### How?
Wrap the date parameter with `new Date()` before passing it to
`Intl.DateTimeFormat().format()` to properly convert the timestamp to a
Date object.
Fixes#14016
Follow-up to #14012.
Once live preview conditions have passed, it is jarring for the live
preview window to suddenly appear. It should be that, despite
preferences, if the live preview window did not _load_ active, then it
should not become active until the user explicitly toggles it on.
This is especially poor UX while creating a new doc. If the conditional
URL is based on a field that has't been filled yet, upon filling that
field (with autosave), live preview suddenly appears and the entire page
shifts mid-edit.
Before:
https://github.com/user-attachments/assets/0da75306-eed3-4a77-bc58-d8a8dd0254bf
After:
https://github.com/user-attachments/assets/c7918601-959d-4ac5-b168-066afc3d879d
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211534009142634
### What?
- Fixes an issue where the table header row was overlapping menu items
in the ListControls component.
### Why?
- The overlapping occurred because the table lacked proper stacking
context, causing z-index rules to affecting the visibility and usability
of menu items in the ListControls component rendered above the table.
### How?
- Added the CSS property isolation: isolate to the table styles. This
creates a new stacking context, ensuring that the table header does not
interfere with overlapping UI elements
### UI Changes:
Before:
<img width="1357" height="730" alt="image"
src="https://github.com/user-attachments/assets/703f7bbe-0cd7-4dfb-a584-a6436a0ea5d7"
/>
After:
<img width="1357" height="730" alt="image"
src="https://github.com/user-attachments/assets/927c904a-4423-420b-ad4d-0a3c4624b5ee"
/>
Fixes#13941
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
Fixes a bug where dragging a file into an upload dropzone when the
drawer is closed throws an error: "Cannot read properties of undefined".
### Why?
The `collectionConfig` variable can be undefined when the drawer is not
opened, causing the code to fail when trying to access
`collectionConfig.upload?.displayPreview`.
### How?
Added optional chaining (`?.`) to safely access `collectionConfig`,
changing `collectionConfig.upload?.displayPreview` to
`collectionConfig?.upload?.displayPreview`.
Fixes#13999
### What?
This PR adds a new `admin.disableRowTypes` config to `'join'` fields
which hides the `"Type"` column from the relationship table.
### Why?
While the collection type column _can be_ helpful for providing
information, it's not always necessary and can sometimes be redundant
when the field only has a singular relationTo. Hiding it can be helpful
by removing visual noise and providing editors the data directly.
### How?
By threading `admin.disableRowTypes` directly to the `getTableState`
function of the `RelationshipTable` component.
**With row types** (via `admin.disableRowTypes: false | undefined` OR
default for polymorphic):

**Without row types** (default for monomorphic):

### What?
When `?locale=` is present in an admin panel URL and that admin panel
URL is visited in an unauthenticated browser, a runtime error is thrown.
### Why?
`upsertPreferences` relies on `req.user` to successfully create a new
`payload-preferences` document. When an unauthenticated user visits a
URL with a `locale` parameter, `upsertPreferences` is called but
`req.user` is not available.
### How?
Prevent `upsertPreferences` from being called when `req.user` is not
available.
Fixes#13581
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
Fixes https://github.com/payloadcms/payload/issues/13308
Adds support for a custom live preview component back, we previously
supported this and it was allowed via the config types but it wasn't
being rendered.
Now we export the `useLivePreviewContext` hook and the original
`LivePreviewWindow` component too so that end users can wrap the live
preview functionality with anything custom that they may need
### What?
Add various missing translations for Icelandic language and update some
of the current ones to be more human readable.
### Why?
To make it more useable in Icelandic
Supports live preview conditions. This is essentially access control for
live preview, where you may want to restrict who can use it based on
certain criteria, such as the current user or document data.
To do this, simply return null or undefined from your live preview url
functions:
```ts
url: ({ req }) => (req.user?.role === 'admin' ? '/hello-world' : null)
```
This is also useful for pages which derive their URL from document data,
e.g. a slug field, do not attempt to render the live preview window
until the URL is fully formatted.
For example, if you have a page in your front-end with the URL structure
of `/posts/[slug]`, the slug field is required before the page can
properly load. However, if the slug is not a required field, or when
drafts and/or autosave is enabled, the slug field might not yet have
data, leading to `/posts/undefined` or similar.
```ts
url: ({ data }) => data?.slug ? `/${data.slug}` : null
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211513433305000
### What?
Adds a new `disableGroupBy` admin config property for fields to control
their visibility in the list view GroupBy dropdown.
### Why
Previously, the GroupByBuilder was incorrectly using `disableListFilter`
to determine which fields to show in the group-by dropdown.
### How
- Added new `disableGroupBy` property to the field admin config types.
- Updated `GroupByBuilder` to filter fields based on `disableGroupBy`
instead of `disableListFilter`
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211511898438807
This PR updates the build process to generate a single
`dist/index.bundled.d.ts` file that bundles all `payload` package types.
Having one bundled declaration file makes it easy to load types into the
Monaco editor (e.g. for the new Code block), enabling full type
completion for the `payload` package.
## Example
```ts
BlocksFeature({
blocks: [
CodeBlock({
slug: 'PayloadCode',
languages: {
ts: 'TypeScript',
},
typescript: {
fetchTypes: [
{
filePath: 'file:///node_modules/payload/index.d.ts',
url: 'https://unpkg.com/payload@3.59.0-internal.e247081/dist/index.bundled.d.ts', // <= download bundled .d.ts
},
],
paths: {
payload: ['file:///node_modules/payload/index.d.ts'],
},
typeRoots: ['node_modules/@types', 'node_modules/payload'],
},
}),
],
}),
```
<img width="1506" height="866" alt="Screenshot 2025-10-01 at 12 38
54@2x"
src="https://github.com/user-attachments/assets/135b9b69-058a-42b9-afa0-daa328f64f38"
/>
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211524241290884
When you have a `hasMany: true` relationship field with at least 1 ID
that references nothing (because the actual document was deleted and
since MongoDB doesn't have foreign constraints - the relationship field
still includes that "dead" ID) graphql querying of that field fails.
This PR fixes it.
The same applies if you don't have access to some document for all DBs
### What?
This PR applies `mergeFieldStyles` to the `ArrayFieldComponent`
component, ensuring that custom admin styles such as `width` are
correctly respected when Array fields are placed inside row layouts.
### Why?
Previously, Array fields did not inherit or apply their `admin.width`
(or other merged field styles). For example, when placing two array
fields side by side inside a row with `width: '50%'`, the widths were
ignored, causing layout issues.
### How?
- Imported and used `mergeFieldStyles` within `ArrayFieldComponent`.
- Applied the merged styles to the root `<div>` via the `style` prop,
consistent with how other field components (like `TextField`) handle
styles.
Fixes#13973
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211511898438801
Sets `reportUnusedDisableDirectives: 'error'` in our eslint config. This
will error when `// eslint-disable-*` directives are unused. It will
also auto-fix if possible.
### What?
Prevents "not found" error when trashing search-enabled documents in
localized site.
### Why?
**See issue https://github.com/payloadcms/payload/issues/13835 for
details and reproduction of bug.**
When a document is soft-deleted (has `deletedAt` timestamp), the search
plugin's `afterChange` hook tries to sync the document but fails because
`payload.findByID()` excludes trashed documents by default.
**Original buggy code** in
`packages/plugin-search/src/utilities/syncDocAsSearchIndex.ts` at lines
46-51:
```typescript
docToSyncWith = await payload.findByID({
id,
collection,
locale: syncLocale,
req,
// MISSING: trash parameter!
})
```
### How?
Added detection for trashed documents and include `trash: true`
parameter:
```typescript
// Check if document is trashed (has deletedAt field)
const isTrashDocument = doc && 'deletedAt' in doc && doc.deletedAt
docToSyncWith = await payload.findByID({
id,
collection,
locale: syncLocale,
req,
// Include trashed documents when the document being synced is trashed
trash: isTrashDocument,
})
```
### Test Coverage Added
- **Enabled trash functionality** in Posts collection for plugin-search
tests
- **Added comprehensive e2e test case** in
`test/plugin-search/int.spec.ts` that verifies:
1. Creates a published post and verifies search document creation
2. Soft deletes the post (moves to trash)
3. Verifies search document is properly synced after trash operation
4. Cleans up by permanently deleting the trashed document
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
### What?
This PR adds atomic array operations ($append and $remove) for
relationship fields with `hasMany: true` across all database adapters.
These operations allow developers to add or remove specific items from
relationship arrays without replacing the entire array.
New API:
```
// Append relationships (prevents duplicates)
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
categories: { $append: ['featured', 'trending'] }
}
})
// Remove specific relationships
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
tags: { $remove: ['draft', 'private'] }
}
})
// Works with polymorphic relationships
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
relatedItems: {
$append: [
{ relationTo: 'categories', value: 'category-id' },
{ relationTo: 'tags', value: 'tag-id' }
]
}
}
})
```
### Why?
Currently, updating relationship arrays requires replacing the entire
array which requires fetching existing data before updates. Requiring
more implementation effort and potential for errors when using the API,
in particular for bulk updates.
### How?
#### Cross-Adapter Features:
- Polymorphic relationships: Full support for relationTo:
['collection1', 'collection2']
- Localized relationships: Proper locale handling when fields are
localized
- Duplicate prevention: Ensures `$append` doesn't create duplicates
- Order preservation: Appends to end of array maintaining order
- Bulk operations: Works with `updateMany` for bulk updates
#### MongoDB Implementation:
- Converts `$append` to native `$addToSet` (prevents duplicates in
contrast to `$push`)
- Converts `$remove` to native `$pull` (targeted removal)
#### Drizzle Implementation (Postgres/SQLite):
- Uses optimized batch `INSERT` with duplicate checking for `$append`
- Uses targeted `DELETE` queries for `$remove`
- Implements timestamp-based ordering for performance
- Handles locale columns conditionally based on schema
### Limitations
The current implementation is only on database-adapter level and not
(yet) for the local API. Implementation in the localAPI will be done
separately.