Commit Graph

2629 Commits

Author SHA1 Message Date
Patrik
1d1240fd13 feat: adds admin.formatDocURL function to control list view linking (#13773)
### What?

Adds a new `formatDocURL` function to collection admin configuration
that allows users to control the linkable state and URLs of first column
fields in list views.

### Why?

To provide a way to disable automatic link creation from the first
column or provide custom URLs based on document data, user permissions,
view context, and document state.

### How?

- Added `formatDocURL` function type to `CollectionAdminOptions` that
receives document data, default URL, request context, collection slug,
and view type
- Modified `renderCell` to call the function when available and handle
three return types:
  - `null`: disables linking entirely
  - `string`: uses custom URL
  - other: falls back to no linking for safety
- Added function to server-only properties to prevent React Server
Components serialization issues
- Updated `DefaultCell` component to support custom `linkURL` prop


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211211792037945
2025-09-24 12:20:54 -07:00
Patrik
512a8fa19f fix: server component readonly state issues with document locking (#13878)
### What?

Fixed two bugs with readonly state for server-rendered components (like
richtext fields and custom server fields):

1. Server components remained readonly after a user took over a locked
document
2. Server components were not readonly when viewing in "read only" mode
until page refresh

### Why?

Both issues stemmed from server-rendered components using their initial
readonly state that was baked in during server-side rendering, rather
than respecting dynamic readonly state changes:

1. **Takeover bug**: When a user took over a locked document,
client-side readonly state was updated but server components continued
using their initial readonly state because the server-side state wasn't
refreshed properly.

2. **Read-only view bug**: When entering "read only" mode, server
components weren't immediately updated to reflect the new readonly state
without a page refresh.

The root cause was that server-side `buildFormState` was called with
`readOnly: isLocked` during initial render, and individual field
components used this initial state rather than respecting dynamic
document-level readonly changes.

### How?

1. **Fixed race condition in `handleTakeOver`**: Made the function async
and await the `updateDocumentEditor` call before calling
`clearRouteCache()` to ensure the database is updated before page reload

2. **Improved editor comparison in `getIsLocked`**: Used `extractID()`
helper to properly compare editor IDs when the editor might be a
reference object

3. **Ensured cache clearing for all takeover scenarios**: Call
`clearRouteCache()` for both DocumentLocked modal and DocumentControls
takeovers to refresh server-side state

4. **Added Form key to force re-render**: Added `key={isLocked}` to the
Form component so it re-renders when the lock state changes, ensuring
all child components get fresh readonly state for both takeover and
read-only view scenarios


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211373627247885
2025-09-24 12:11:10 -07:00
Jarrod Flesch
fcb8b5a066 feat(plugin-multi-tenant): improves tenant assignment flow (#13881)
### Improved tenant assignment flow
This PR improves the tenant assignment flow. I know a lot of users liked
the previous flow where the field was not injected into the document.
But the original flow, confused many of users because the tenant filter
(top left) was being used to set the tenant on the document _and_ filter
the list view.

This change shown below is aiming to solve both of those groups with a
slightly different approach. As always, feedback is welcome while we try
to really make this plugin work for everyone.


https://github.com/user-attachments/assets/ceee8b3a-c5f5-40e9-8648-f583e2412199

Added 2 new localization strings:

```
// shown in the 3 dot menu
'assign-tenant-button-label': 'Assign Tenant',

// shown when needing to assign a tenant to a NEW document
'assign-tenant-modal-title': 'Assign "{{title}}"',
```

Removed 2 localization strings:
```
'confirm-modal-tenant-switch--body',
'confirm-modal-tenant-switch--heading'
```
2025-09-24 13:19:33 -04:00
Patrik
3f5c989954 fix(ui): array field state to return empty array instead of 0 (#11283)
### What?

This PR fixes an issue where empty array fields would return `0` instead
of an empty array `[]` in form state.

The issue was caused by `rows` being initialized as `undefined` within
the array field reducer.

As a result, `rows` did not exist on array field state when initial
state was empty.

This has been updated to initialize as an empty array (`rows: []`) to
ensure consistent behavior when using `getDataByPath`.

Fixes #10712 


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211439284995184
2025-09-24 13:17:30 -04:00
Sasha
f980a86bd6 fix(db-sqlite): exists operator in createJSONQuery (#13907)
Fixes https://github.com/payloadcms/payload/issues/13882
2025-09-24 18:42:33 +03:00
Alessio Gravili
59414bd8f1 feat(richtext-lexical): support copy & pasting and drag & dopping files/images into the editor (#13868)
This PR adds support for inserting images into the rich text editor via
both **copy & paste** and **drag & drop**, whether from local files or
image DOM nodes.

It leverages the bulk uploads UI to provide a smooth workflow for:
- Selecting the target collection
- Filling in any required fields defined on the uploads collection
- Uploading multiple images at once

This significantly improves the UX for adding images to rich text, and
also works seamlessly when pasting images from external editors like
Google Docs or Microsoft Word.

Test pre-release: `3.57.0-internal.801ab5a`

## Showcase - drag & drop images from computer


https://github.com/user-attachments/assets/c558c034-d2e4-40d8-9035-c0681389fb7b

## Showcase - copy & paste images from computer


https://github.com/user-attachments/assets/f36faf94-5274-4151-b141-00aff2b0efa4

## Showcase - copy & paste image DOM nodes


https://github.com/user-attachments/assets/2839ed0f-3f28-4e8d-8b47-01d0cb947edc

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211217132290841
2025-09-24 15:04:46 +00:00
Jessica Rynkar
062c1d7e89 fix: pagination returning duplicate results when timestamps: false (#13920)
### What
Fixes a bug where collection docs paginate incorrectly when `timestamps:
false` is set — the same docs were appearing across multiple pages.

### Why
The `find` query sanitizes the `sort` parameter.  

- With `timestamps: true`, it defaults to `createdAt`
- With `timestamps: false`, it falls back to `_id`

That logic is correct, but in `find.ts` we always passed `timestamps:
true`, ignoring the collection config. With the right sort applied,
pagination works as expected.

### How
`find.ts` now passes `collectionConfig.timestamps` to
`buildSortParam()`, ensuring the correct sort field is chosen.

---

Fixes #13888
2025-09-24 14:44:41 +01:00
Patrik
96c66125dd fix(plugin-import-export): collectionSlug field regression from hidden field changes (#13917)
### What?

- Fixes collectionSlug field not being populated, breaking export
downloads
- Works around recent UI changes that prevent custom components from
rendering for `admin.hidden` fields
- Removes `admin.hidden: true` - as the component already ensures a
hidden state by returning `null`

### Why?

- This merged [PR](https://github.com/payloadcms/payload/pull/13869)
changed how `admin.hidden` fields are rendered, causing them to bypass
custom field components entirely. The `collectionSlug` field relied on a
custom `CollectionField` component to set its value from the
ImportExportProvider context.

### How?

- Removes `admin.hidden: true`
- Field remains visually hidden with `null` return but custom component
logic now executes properly
2025-09-24 06:01:02 -07:00
Patrik
9c20eb34d4 fix(next): groupBy for polymorphic relationships (#13781)
### What?

Fixed groupBy functionality for polymorphic relationships, which was
throwing errors.

<img width="1099" height="996" alt="Screenshot 2025-09-11 at 3 10 32 PM"
src="https://github.com/user-attachments/assets/bd11d557-7f21-4e09-8fe6-6a43d777d82c"
/>

### Why?

The groupBy feature failed for polymorphic relationships because:
- `relationshipConfig` was undefined when `relationTo` is an array
(polymorphic)
- ObjectId serialization errors when passing database objects to React
client components
- hasMany relationships weren't properly flattened into individual
groups
- "No Value" groups appeared first instead of populated groups

### How?

- Handle polymorphic relationship structure `{relationTo, value}`
correctly by finding the right collection config using `relationTo`
- Add proper collection config lookup for each relation in polymorphic
relationships during populate
- Flatten hasMany relationship arrays so documents with `[Category1,
Category2]` create separate groups for each

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211331842191589
2025-09-23 10:14:35 -07:00
Jacob Fletcher
00a673e491 feat(next): regenerate live preview url on save (#13631)
Closes #12785.

Although your live preview URL can be dynamic based on document data, it
is never recalculated after initial mount. This means if your URL is
dependent of document data that was just changed, such as a "slug"
field, the URL of the iframe does not reflect that change as expected
until the window is refreshed or you navigate back.

This also means that server-side live preview will crash when your
front-end attempts to query using a slug that no longer exists. Here's
the general flow: slug changes, autosave runs, iframe refreshes (url has
old slug), 404.

Now, we execute your live preview function on submit within form state,
and the window responds to the new URL as expected, refreshing itself
without losing its connection.

Here's the result:


https://github.com/user-attachments/assets/7dd3b147-ab6c-4103-8b2f-14d6bc889625

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211094211063140
2025-09-23 09:37:15 -04:00
Alessio Gravili
d8dace6509 feat: conditional blocks (#13801)
This PR introduces support for conditionally setting allowable block
types via a new `field.filterOptions` property on the blocks field.

Closes the following feature requests:
https://github.com/payloadcms/payload/discussions/5348,
https://github.com/payloadcms/payload/discussions/4668 (partly)

## Example

```ts
fields: [
  {
      name: 'enabledBlocks',
      type: 'text',
      admin: {
        description:
          "Change the value of this field to change the enabled blocks of the blocksWithDynamicFilterOptions field. If it's empty, all blocks are enabled.",
      },
    },
    {
      name: 'blocksWithFilterOptions',
      type: 'blocks',
      filterOptions: ['block1', 'block2'],
      blocks: [
        {
          slug: 'block1',
          fields: [
            {
              type: 'text',
              name: 'block1Text',
            },
          ],
        },
        {
          slug: 'block2',
          fields: [
            {
              type: 'text',
              name: 'block2Text',
            },
          ],
        },
        {
          slug: 'block3',
          fields: [
            {
              type: 'text',
              name: 'block3Text',
            },
          ],
        },
      ],
    },
    {
      name: 'blocksWithDynamicFilterOptions',
      type: 'blocks',
      filterOptions: ({ siblingData: _siblingData, data }) => {
        const siblingData = _siblingData as { enabledBlocks: string }

        if (siblingData?.enabledBlocks !== data?.enabledBlocks) {
          // Just an extra assurance that the field is working as intended
          throw new Error('enabledBlocks and siblingData.enabledBlocks must be identical')
        }
        return siblingData?.enabledBlocks?.length ? [siblingData.enabledBlocks] : true
      },
      blocks: [
        {
          slug: 'block1',
          fields: [
            {
              type: 'text',
              name: 'block1Text',
            },
          ],
        },
        {
          slug: 'block2',
          fields: [
            {
              type: 'text',
              name: 'block2Text',
            },
          ],
        },
        {
          slug: 'block3',
          fields: [
            {
              type: 'text',
              name: 'block3Text',
            },
          ],
        },
      ],
    },
]
```


https://github.com/user-attachments/assets/e38a804f-22fa-4fd2-a6af-ba9b0a5a04d2

# Rationale

## Why not `block.condition`?

- Individual blocks are often reused in multiple contexts, where the
logic for when they should be available may differ. It’s more
appropriate for the blocks field (typically tied to a single collection)
to determine availability.
- Hiding existing blocks when they no longer satisfy a condition would
cause issues - for example, reordering blocks would break or cause block
data to disappear. Instead, this implementation ensures consistency by
throwing a validation error if a block is no longer allowed. This aligns
with the behavior of `filterOptions` in relationship fields, rather than
`condition`.

## Why not call it `blocksFilterOptions`?

Although the type differs from relationship fields, this property is
named `filterOptions` (and not `blocksFilterOptions`) for consistency
across field types. For example, the Select field also uses
`filterOptions` despite its type being unique.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211334752795631
2025-09-22 14:20:25 -07:00
Sasha
e99e054d7c fix: findDistinct with polymorphic relationships (#13875)
Fixes `findDistinct` with polymorphic relationships and also fixes a bug
from https://github.com/payloadcms/payload/pull/13840 when
`findDistinct` didn't work properly for `hasMany` relationships in
mongodb if `sort` is the same as `field`

---------

Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
2025-09-22 12:23:17 -07:00
Alessio Gravili
66f5d1429d fix(richtext-lexical): richtext field duplicates description custom component (#13880)
The lexical field component was accidentally rendering the description
component twice.

Fixes https://github.com/payloadcms/payload/issues/13644
2025-09-22 14:39:55 -04:00
Alessio Gravili
228e8f281a fix: admin.hidden not respected for RSCs, render-field server function not respecting field-overrides client-side (#13869)
This PR fixes 2 issues:

- the `fieldConfig.admin.hidden` property had no effect for react server
components, because the RSC was returned before we're checking for
`admin.hidden` in RenderFields
- the `render-field` server function did not propagate fieldConfig
overrides to the clientProps. This means overriding `admin.Label` had no
effect

Adds e2e tests for both issues
2025-09-22 14:36:41 -04:00
Sasha
1072171f97 feat: support hasMany virtual relationship fields (#13879)
This PR adds support for the following configuration:
```ts
const config = {
  collections: [
    {
      slug: 'categories',
      fields: [
        {
          name: 'title',
          type: 'text',
        },
      ],
    },
    {
      slug: 'posts',
      fields: [
        {
          name: 'title',
          type: 'text',
        },
        {
          name: 'categories',
          type: 'relationship',
          relationTo: 'categories',
          hasMany: true,
        },
      ],
    },
    {
      slug: 'examples',
      fields: [
        {
          name: 'postCategoriesTitles',
          type: 'text',
          virtual: 'post.categories.title',
          // hasMany: true - added automatically during the sanitization
        },
        {
          type: 'relationship',
          relationTo: 'posts',
          name: 'post',
        },
        {
          name: 'postsTitles',
          type: 'text',
          virtual: 'posts.title',
          // hasMany: true - added automatically during the sanitization
        },
        {
          type: 'relationship',
          relationTo: 'posts',
          name: 'posts',
          hasMany: true,
        },
      ],
    },
  ],
}
```

In the result:
`postsTitles` - will be always populated with an array of posts titles.
`postCategoriesTitles` - will be always populated with an array of the
categories titles that are related to this post

The virtual `text` field is sanitizated to `hasMany: true`
automatically, but you can specify that manually as well.
2025-09-19 21:04:07 +03:00
German Jablonski
207caa570c fix(richtext-lexical): prevent disallowed headings to be pasted (#13765)
Fixes #13705

With this PR, if the editor detects a disallowed heading in
`HeadingFeature`, it automatically converts it to the lowest allowed
heading.

I've also verified that disallowed headings aren't introduced when
pasting from the clipboard if the HeadingFeature isn't registered at
all. The reason this works is because the LexicalEditor doesn't have the
HeadingNode in that case.
2025-09-19 11:02:18 -07:00
Sasha
22ae9fa9d0 fix(db-postgres): localized relationships inside blocks (#13760)
Fixes https://github.com/payloadcms/payload/issues/12463 and
https://github.com/payloadcms/payload/issues/13747
2025-09-19 09:28:12 -04:00
Alessio Gravili
1c89291fac feat(richtext-lexical): utility render lexical field on-demand (#13657)
## Why this exists

Lexical in Payload is a React Server Component (RSC). Historically that
created three headaches:

1. You couldn’t render the editor directly from the client.
2. Features like blocks, tables, upload and link drawers require the
server to know the shape of nested sub‑fields at render time. If you
tried to render on demand, the server didn’t know those schemas.
3. The rich text field is designed to live inside a Form. For simple use
cases, setting up a full form just to manage editor state was
cumbersome.

## What’s new

We now ship a client component, `<RenderLexical />`, that renders a
Lexical editor **on demand** while still covering the full feature set.
On mount, it calls a server action to render the editor on the server
using the new `render-field` server action. That server render gives
Lexical everything it needs (including nested field schemas) and returns
a ready‑to‑hydrate editor.

## Example - Rendering in custom component within existing Form

```tsx
'use client'

import type { JSONFieldClientComponent } from 'payload'

import { buildEditorState, RenderLexical } from '@payloadcms/richtext-lexical/client'

import { lexicalFullyFeaturedSlug } from '../../slugs.js'

export const Component: JSONFieldClientComponent = (args) => {
  return (
    <div>
      Fully-Featured Component:
      <RenderLexical
        field={{ name: 'json' }}
        initialValue={buildEditorState({ text: 'defaultValue' })}
        schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`}
      />
    </div>
  )
}
```

## Example - Rendering outside of Form, manually managing richText
values

```ts
'use client'

import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
import type { JSONFieldClientComponent } from 'payload'

import { buildEditorState, RenderLexical } from '@payloadcms/richtext-lexical/client'
import React, { useState } from 'react'

import { lexicalFullyFeaturedSlug } from '../../slugs.js'

export const Component: JSONFieldClientComponent = (args) => {
  const [value, setValue] = useState<DefaultTypedEditorState | undefined>(() =>
    buildEditorState({ text: 'state default' }),
  )

  const handleReset = React.useCallback(() => {
    setValue(buildEditorState({ text: 'state default' }))
  }, [])

  return (
    <div>
      Default Component:
      <RenderLexical
        field={{ name: 'json' }}
        initialValue={buildEditorState({ text: 'defaultValue' })}
        schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`}
        setValue={setValue as any}
        value={value}
      />
      <button onClick={handleReset} style={{ marginTop: 8 }} type="button">
        Reset Editor State
      </button>
    </div>
  )
}
```

## How it works (under the hood)

- On first render, `<RenderLexical />` calls the server function
`render-field` (wired into @payloadcms/next), passing a schemaPath.
- The server loads the exact field config and its client schema map for
that path, renders the Lexical editor server‑side (so nested features
like blocks/tables/relationships are fully known), and returns the
component tree.
- While waiting, the client shows a small shimmer skeleton.
- Inside Forms, RenderLexical plugs into the parent form via useField;
outside Forms, you can fully control the value by passing
value/setValue.

## Type Improvements

While implementing the `buildEditorState` helper function for our test
suite, I noticed some issues with our `TypedEditorState` type:
- nodes were no longer narrowed by their node.type types
- upon fixing this issue, the type was no longer compatible with the
generated types. To address this, I had to weaken the generated type a
bit.

In order to ensure the type will keep functioning as intended from now
on, this PR also adds some type tests

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211110462564644
2025-09-18 15:01:12 -07:00
Jarrod Flesch
83b6e3757d fix(graphql): graphql tab schema not handling tabs correctly (#13850)
Fixes https://github.com/payloadcms/payload/issues/13833

When generating graphql schemas, named tabs were not properly being
accounted for. This PR fixes that and ensure that the correct schema
types are generated for named tabs.
2025-09-18 09:34:50 -07:00
Sasha
d0543a463f fix: support hasMany: true relationships in findDistinct (#13840)
Previously, the `findDistinct` operation didn't work correctly for
relationships with `hasMany: true`. This PR fixes it.
2025-09-17 18:21:24 +03:00
Alessio Gravili
8d3b146d2d fix(next): unnamed, unlabeled groups displayed without label in version view (#13831)
Before, unnamed, unlabelled group were part of an unlabeled collapsible
in the version diff view. This means, all it displayed was a clickable,
empty rectangle. This looks like a bug rather than intended.

This PR displays a new <Unnamed Group> label in these cases.

It also fixes the incorrect `NamedGroupFieldClient` type. Previously,
that type did not include the `name` property, even though it was
available on the client.

Before:
<img width="2372" height="688" alt="Screenshot 2025-09-16 at 18 57
45@2x"
src="https://github.com/user-attachments/assets/0f351f84-a00f-4067-aa40-d0e8fbfd5f0b"
/>


After:

<img width="2326" height="598" alt="Screenshot 2025-09-16 at 18 56
14@2x"
src="https://github.com/user-attachments/assets/bddee841-8218-4a90-a052-9875c5f252c0"
/>


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211375615406676
2025-09-17 11:38:07 +03:00
Alessio Gravili
433c5136fc fix(next): sparse block and arrays diffs were not rendered correctly (#13829)
Assuming you have 3 block/array rows and you only modify the middle one
- the version view would still display row 1 and row 3:

<img width="2354" height="1224" alt="Screenshot 2025-09-16 at 16 15
22@2x"
src="https://github.com/user-attachments/assets/5f823276-fda2-4192-a7d3-482f2a2228f9"
/>

After this PR, it's now displayed correctly:

<img width="2368" height="980" alt="Screenshot 2025-09-16 at 16 15
09@2x"
src="https://github.com/user-attachments/assets/7fc5ee25-f925-4c41-b62a-9b33652e19f9"
/>

## The Fix

The generated version fields will contain holes in the `rows` array for
rows that have no changes. The fix is to simply skip rendering those
rows. We still need to keep those holes in order to maintain the correct
row indexes.

Additionally, this PR improves the naming of some legacy variables and
function arguments that I missed out on during the version view
overhaul:

> comparison => valueFrom
> version => valueTo

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211267837905382
2025-09-17 11:36:34 +03:00
Jarrod Flesch
dc732b8f52 fix(ui): populate nested fields for enableListViewSelectAPI (#13827)
Fixes an issue with the new experimental `enableListViewSelectAPI`
config option.

Group fields were not populating properly in the list view

### Before (incorrect)
```ts
{
  group.field: true
}
```

### After (correct)
```ts
{
  group: {
    field: true
  }
}
```
2025-09-16 21:23:08 +00:00
Sasha
a9553925f6 fix: add missed pagination property to findVersions and findGlobalVersions and handle it properly (#13763)
* The `pagination` property was missing in `findVersions` and
`findGlobalVersions` Local API operations, although the actual functions
did have it -
1b93c4becc/packages/payload/src/collections/operations/findVersions.ts (L25)
* The handling of the `pagination` property in those functions was
broken, this PR fixes it.
2025-09-16 17:05:54 -04:00
Sasha
24ace70b58 fix(db-mongodb): support 2x and more relationship sorting (#13819)
Previously, sorting like:
`sort: 'relationship.anotherRelationship.title'` (and more nested)
didn't work with the MongoDB adapter, this PR fixes this.
2025-09-16 21:03:48 +03:00
Patrik
3b13867aee fix: skip validation when trashing documents with empty required fields (#13807)
### What?

Skip field validation when trashing documents with empty required
fields.

### Why?

When trashing a document that was saved as a draft with empty required
fields, Payload would run full validation and fail with "The following
fields are invalid" errors. This happened because trash operations were
treated as regular updates that require full field validation, even
though trashing is just a metadata change (setting `deletedAt`) and
shouldn't be blocked by content validation issues.
   
### How?

- Modified `skipValidation` logic in `updateDocument()` to skip
validation when `deletedAt` is being set in the update data

Fixes #13706
2025-09-16 07:09:39 -07:00
Philipp Schneider
a2c31fa44a fix(ui): prevent form validation after draft submit error (#12918)
Follow-up to #12893.

### What?

Removes cases where submitting a document as a draft in a collection
with versioning enabled would cause publishing validation errors to be
displayed on further document form changes. An example case is when
saving the draft failed due to a `beforeChange` hook throwing an
`APIError`.

### How

The behavior change is that the form state is marked as un-submitted
post a submit failure as a draft. The form not being considered as
submitted results in `packages/ui/src/views/Edit/index.tsx` to use
`skipValidation: true`.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-09-15 17:10:47 -04:00
Patrik
faed3aaf26 fix: versions created with incomplete data when using select parameter (#13809)
### What?

Remove `select` parameter from database operations in update operation
during version creation to fix malformed version data.

### Why?

The `select` parameter was being passed to `saveVersion()` in update
operations, causing versions to only contain selected fields

### How?

- Removed `select` parameter from `payload.db.updateOne()` calls in
update operations
- Removed `select` parameter from `saveVersion()` call in update
operation


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211334352350013
2025-09-15 14:00:04 -07:00
Sasha
09dec43c15 fix(db-mongodb): localized arrays inside blocks with versions (#13804)
Fixes https://github.com/payloadcms/payload/issues/13745
2025-09-15 14:31:25 -04:00
Alessio Gravili
b34e5eadf4 fix(live-preview): client-side live preview failed to populate localized fields (#13794)
Fixes #13756 

The findByID endpoint, by default, expects the data for localized fields
to be an object, values mapped to the locale. This is not the case for
client-side live preview, as we send already-flattened data to the
findByID endpoint.

For localized fields where the value is an object (richText/json/group),
the afterRead hook handler would attempt to flatten the field value,
even though it was already flattened.

## Solution

The solution is to expose a `flattenLocales` arg to the findByID
endpoint (default: true) and pass `flattenLocales: false` from the
client-side live preview request handler.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211334752795627
2025-09-15 12:16:52 -04:00
Jacob Fletcher
8113d3bdef fix(next): exclude permissions from page response when unauthenticated (#13796)
Similar spirit as #13714.

Permissions are embedded into the page response, exposing some field
names to unauthenticated users.

For example, when setting `read: () => false` on a field, that field's
name is now included in the response due to its presence in the
permissions object.

We now search the HTML source directly in the test, similar to "view
source" in the browser, which will be much effective at preventing
regression going forward.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211347942663256
2025-09-12 20:57:03 +00:00
Jacob Fletcher
e2632c86d0 fix: fully sanitize unauthenticated client config (#13785)
Follow-up to #13714.

Fully sanitizes the unauthenticated client config to exclude much of the
users collection, including fields, etc. These are not required of the
login flow and are now completely omitted along with other unnecessary
properties.

This is closely aligned with the goals of the original PR, and as an
added bonus, makes the config _even smaller_ than it already was for
unauthenticated users.

Needs #13790.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211332845301588
2025-09-12 14:52:50 -04:00
Anders Semb Hermansen
b62a30a8dc fix(live-preview): reset cache per subscription and ignore invalid preview messages (#13793)
### What?

Fix two live preview issues affecting client-side navigation:
1. Stale preview data leaking between pages using `useLivePreview`.
2. Erroneous fetches to `/api/undefined` and incorrect content rendering
when preview events lack slugs.

### Why?

The live-preview module cached merged preview data globally, which
persisted across route changes, causing a new page to render with the
previous page’s data.

The client attempted to merge when preview events didn’t specify
collectionSlug or globalSlug, producing an endpoint of undefined and
triggering requests to /api/undefined, sometimes overwriting state with
mismatched content.

### How?

Clear the internal cache at the time of `subscribe()` so each page using
`useLivePreview` starts from a clean slate.

In `handleMessage`, only call `mergeData` when `collectionSlug` or
`globalSlug` is present; otherwise return `initialData` and perform no
request.

Fixes #13792
2025-09-12 18:40:24 +00:00
Jacob Fletcher
dfb0021545 fix: client config context inheritance (#13790)
Redirecting from login to any non-auth collection crashes the page with
the following error:

```
Cannot read properties of null (reading 'fields')
```

TL;DR: the page-level config context was threading stale methods from
the root config provider.

#### Background

The client config is now gated behind authentication as of #13714, so it
changes based on authentication status. If the root layout is mounted
before authentication, it puts the unauthenticated client config into
state for the entire app to consume.

On login, the root layout does not re-render, so the page itself needs
to generate a fresh client config and sync it up.

This leads to race conditions, however, where if the login page included
a `?redirect=` param, the redirect would take place _before_ the
page-level client config could sync to the layout, and ultimately crash
the page. This was addressed in #13786.

While this fixed redirects to the "users" collection, this collection is
_already_ included in the client config (soon to be omitted by #13785).
So if you redirect to any other collection, the above error occurs.

#### Problem

The page-level config context is only overriding the `config` property,
keeping stale methods from the root config provider. This means calling
`getEntityConfig` during this moment in the time would reference the
stale config, although `config` itself would be fresh.

#### Solution

Wrap the page with an entirely new context provider. Do not thread
inherited methods from the root provider, this way all new methods get
instantiated using the fresh config.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211332845301596
2025-09-12 17:43:03 +00:00
Anders Semb Hermansen
4278e724f5 fix(next): richtext field is read-only for expired lock (#13789)
### What?

Opening a document with an expired lock and richtext caused the richtext
to be read-only.

### Why?

The changes in #13579 made the richtext read only if isLocked is set.
But the server side implementation of isLocked did not consider expired
locks.

### How?

Update the server-side getIsLocked to also consider expired locks by not
loading them.
2025-09-12 07:46:51 -07:00
Alessio Gravili
13af91f05d fix(next): login redirect crashes page (#13786)
## Problem

When logging in with a `?redirect=`, the target page may crash. This
happens because the update from `SyncClientConfig` is applied in a
`useEffect`, which runs **_after_** the target page's client components
render. As a result, those components read the unauthenticated client
config on first render - even though they expect the authenticated one.

## Steps To Reproduce

1. logout
2. login with ?redirect=
3. document client component incorrectly still receives unauthenticated
client config on render
4. THEN the SyncClientConfig useEffect runs. Too late
5. Potential error (depending on the page, e.g. document view) -
document client component expects sth to be there, but it is not

## Solution

This PR replaces `SyncClientConfig` with a `RootPageConfigProvider`.
This new provider shadows the root layout’s `ConfigProvider` and ensures
the correct client config is available **_immediately_**.

It still updates the config using `useEffect` (the same
way`SyncClientConfig` did), but with one key difference:

- During the brief window between the redirect and the effect running,
it overrides the root layout’s config and provides the fresh,
authenticated config from the root page via the `RootConfigContext`.

This guarantees that client components on the target page receive the
correct config on first render, preventing errors caused by reading the
outdated unauthenticated config.

## Additional change - get rid of `UnsanitizedClientConfig` and
`sanitizeClientConfig`

Those functions added unnecessary complexity, just to build the
blocksMap. I removed those and perform the building of the `blocksMap`
server-side - directly in `createClientConfig`.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211334752795621
2025-09-12 09:41:24 -04:00
Jacob Fletcher
8a7124a15e fix(next): resolve filterOptions by path (#13779)
Follow up to #11375.

When setting `filterOptions` on relationship or upload fields _that are
nested within a named field_, those options won't be applied to the
`Filter` component in the list view.

This is because of how we key the results when resolving `filterOptions`
on the server. Instead of using the field path as expected, we were
using the field name, causing a failed lookup on the front-end. This
also solves an issue where two fields with the same name would override
each other's `filterOptions`, since field names alone are not unique.

Unrelated: this PR also does some general housekeeping to e2e test
helpers.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211332845301583
2025-09-11 13:24:16 -07:00
Jacob Fletcher
82820312e8 feat: expose multipart/form-data parsing options (#13766)
When sending REST API requests with multipart/form-data, e.g. PATCH or
POST within the admin panel, a request body larger than 1MB throws the
following error:

```
Unterminated string in JSON at position...
```

This is because there are sensible defaults imposed by the HTML form
data parser (currently using
[busboy](https://github.com/fastify/busboy)). If your documents exceed
this limit, you may run into this error when editing them within the
admin panel.

To support large documents over 1MB, use the new `bodyParser` property
on the root config:

```ts
import { buildConfig } from 'payload'

const config = buildConfig({
  // ...
  bodyParser: {
    limits: {
      fieldSize: 2 * 1024 * 1024, // This will allow requests containing up to 2MB of multipart/form-data
    }
  }
}
```

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211317005907885
2025-09-11 09:14:56 -04:00
Jacob Fletcher
3af546eeee feat: global beforeOperation hook (#13768)
Adds support for the `beforeOperation` hook on globals. Runs before all
other hooks to either modify the arguments that operations receive, or
perform side-effects before an operation begins.

```ts
import type { GlobalConfig } from 'payload'

const MyGlobal: GlobalConfig = {
  // ...
  hooks: {
    beforeOperation: []
  }
}
```

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211317005907890
2025-09-10 20:46:49 +00:00
Patrik
e1ea07441e feat: adds disableListColumn and disableListFilter to imageSize admin props (#13699)
### What?

Added support for `disableListColumn` and `disableListFilter` admin
properties on imageSize configurations that automatically apply to all
fields within the corresponding size group.

### Why?

Upload collections with multiple image sizes can clutter the admin list
view with many size-specific columns and filters. This feature allows
developers to selectively hide size fields from list views while keeping
them accessible in the document edit view.

### How?

Modified `getBaseFields.ts` to inherit admin properties from imageSize
configuration and apply them to all nested fields (url, width, height,
mimeType, filesize, filename) within each size group. The implementation
uses conditional spread operators to only apply these properties when
explicitly set to `true`, maintaining backward compatibility.
2025-09-09 11:56:57 -04:00
Jarrod Flesch
6a0637ecb2 feat(ui): save collection folder tab preferences (#13702)
Saves folder preferences when navigating from list to folder view. This
is a UX improvement so users don't need to click by-folder every time
they go back to the list view.
2025-09-09 11:43:00 -04:00
Alessio Gravili
911f17a887 fix(next): version diff view not handling all field permissions correctly (#13721)
Fixes https://github.com/payloadcms/payload/issues/13286

The version diff view did not handle all field permissions correctly,
leading to some fields disappearing if access control was set. This PR
fixes that.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211267837905375
2025-09-09 11:31:15 -04:00
Jacob Fletcher
d2d2df4408 perf(ui): opt-in to the select api in the list view (#13697)
Adds a new property to the config that enables the Select API in the
list view. This is a performance opt-in, where only the active columns
(and those specified in `forceSelect`) are queried. This can greatly
improve performance, especially for collections with large documents or
many fields.

To enable this, use the `admin.enableListViewSelectAPI` in your
Collection Config:

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

export const Posts: CollectionConfig = {
  // ...
  admin: {
    enableListViewSelectAPI: true // This will select only the active columns (and any `forceSelect` fields)
  }
}
```

Note: The `enableListViewSelectAPI` property is currently labeled as
experimental, as it will likely become the default behavior in v4 and be
deprecated. The reason it cannot be the default now is because cells or
other components may be relying on fully populated data, which will no
longer be the case when using `select`.

For example, if your component relies on a "title" field, this field
will _**not**_ exist if the column is **_inactive_**:

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

export const Posts: CollectionConfig = {
  // ...
  fields: [
    // ...
    {
      name: 'myField',
      type: 'text',
      hooks: {
        afterRead: [
          ({ doc }) => doc.title // `title` will only be populated if the column is active
        ]
      }
    }
  ]
}
```

There are other cases that might be affected by this change as well, for
example any components relying on the `data` object returned by the
`useListQuery()` hook:

```ts
'use client'

export const MyComponent = () => {
  const { data } = useListQuery() // `data.docs` will only contain fields that are selected
  
  // ...
}
```

To ensure title is always present, you will need to add that field to
the `forceSelect` property in your Collection Config:

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

export const Posts: CollectionConfig = {
  // ...
  forceSelect: {
    title: true
  }
}
```

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211248751470559
2025-09-08 16:38:00 -04:00
Jacob Fletcher
a528a55639 test: increase timeout in uploads cors test (#13743)
Deflakes the uploads e2e suite. The CORS test was not waiting not long
enough for the response and ultimately fails.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211295974585601
2025-09-08 15:45:13 -04:00
Said Akhrarov
bbcdea5450 fix(next): display deleted relations and uploads in version diff views (#12955)
This PR fixes an issue where `relationship` and `upload` fields that had
their document-counterpart deleted would cause a runtime error. This
happens because the version diff components expected a populated
document instead of an id.

Two e2e tests were also added to the existing suite to test that the
diff view is reachable even after deletions.

### Why?
To prevent a runtime error when viewing diffs for documents that have
relationships to deleted documents.

### How?
Adjusting the diff view to accommodate deleted document relations.

Fixes #12915

Before:


[version-diff-collections-before.mp4](https://private-user-images.githubusercontent.com/20878653/458373129-745a5313-b85a-4ef0-a0d4-a13b52994f6f.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTA5Njc3NDEsIm5iZiI6MTc1MDk2NzQ0MSwicGF0aCI6Ii8yMDg3ODY1My80NTgzNzMxMjktNzQ1YTUzMTMtYjg1YS00ZWYwLWEwZDQtYTEzYjUyOTk0ZjZmLm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA2MjYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNjI2VDE5NTA0MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTUwOTUzMmJmYWI4YmM4ODZjYjhiNWVkOWE0NDgwODRiYjUxNjVkZmNiOWE2NWM3ODVhMTJiY2ViMGQ4MDE4NjYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.Of4Os9WCeOo_aN2kHNMShEgOc4-mYAwc41_E3YsX1tw)

After:


[versions-collections-after-Posts---Payload.webm](https://github.com/user-attachments/assets/ce4031da-b703-4944-857d-9ff627c58fdd)

Notes:
- I renamed `PopulatedRelationshipValue` to `RelationshipValue` as we
don't actually know if the value is populated. Such as in the case of
when the doc was deleted.
2025-09-06 04:52:15 +00:00
Aapo Laakkio
794bf8299c fix(next): version diff view shows correct document title in step nav (#13713)
This PR fixes two bugs in the version diff view SetStepNav component

### Bug 1: Document title isn't shown correctly in the step navigation
if the field of `useAsTitle` is nested inside a presentational field.

The StepNav shows the title of the document consistently throughout
every view except the version diff view. In the version diff view, the
document title is always `[Untitled]` if the field of `useAsTitle` is
nested inside presentational fields. Below is a video demo of the bug:


https://github.com/user-attachments/assets/23cb140a-b6d3-4d39-babf-5e4878651869

This happens because the fields of the collection/global aren't
flattened inside SetStepNav and thus the component is not accessing the
field data correctly. This results in the title being `null` causing the
fallback title to be shown.

### Bug 2: Step navigation shows the title of the version viewed, not
the current version

The StepNav component takes the title of the current version viewed.
This causes the second part of the navigation path to change between
versions which is inconsistent between other views and doesn't seem
intentional, although it could be. Below is a video of the bug with the
first bug fixed by flattening the fields:


https://github.com/user-attachments/assets/e5beb9b3-8e2e-4232-b1e5-5cce720e46b9

This happens due to the fact that the title is taken from the
`useAsTitle` field of the **viewed** version rather than the **current**
version. This bug is fixed by using the `useDocumentTitle` hook from the
ui package instead of passing the version's `useAsTitle` data down the
component tree. The final state of the step navigation is shown in the
following video:


https://github.com/user-attachments/assets/a69d5088-e7ee-43be-8f47-d9775d43dde9

I also added a test to test that the title part in the step navigation
stays consistent between versions and implicitly also tests that the
document title is shown correctly in the step nav if the field of
`useAsTitle` is a nested inside a presentational field.
2025-09-05 15:49:44 -07:00
Jarrod Flesch
9f0573d714 chore(ui): prevent loading orphaned documents when viewing root collection folders (#13684)
Root level collection folders should not display items, only folders. 


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210821658736793
2025-09-05 18:22:18 -04:00
Jarrod Flesch
f288cf6a8f feat(ui): adds admin.autoRefresh root config property (#13682)
Adds the `admin.autoRefresh` property to the root config. This allows
users to stay logged and have their token always refresh in the
background without being prompted with the "Stay Logged In?" modal.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211114366468735
2025-09-05 17:05:18 -04:00
Alessio Gravili
6e203db33c feat(live-preview): client-side live preview: simplify population, support hooks and lexical block population (#13619)
Alternative solution to
https://github.com/payloadcms/payload/pull/11104. Big thanks to
@andershermansen and @GermanJablo for kickstarting work on a solution
and bringing this to our attention. This PR copies over the live-preview
test suite example from his PR.

Fixes https://github.com/payloadcms/payload/issues/5285,
https://github.com/payloadcms/payload/issues/6071 and
https://github.com/payloadcms/payload/issues/8277. Potentially fixes
#11801

This PR completely gets rid of our client-side live preview field
traversal + population and all logic related to it, and instead lets the
findByID endpoint handle it.

The data sent through the live preview message event is now passed to
findByID via the newly added `data` attribute. The findByID endpoint
will then use this data and run hooks on it (which run population),
instead of fetching the data from the database.

This new API basically behaves like a `/api/populate?data=` endpoint,
with the benefit that it runs all the hooks. Another use-case for it
will be rendering lexical data. Sometimes you may only have unpopulated
data available. This functionality allows you to then populate the
lexical portion of it on-the-fly, so that you can properly render it to
JSX while displaying images.

## Benefits
- a lot less code to maintain. No duplicative population logic
- much faster - one single API request instead of one request per
relationship to populate
- all payload features are now correctly supported (population and
hooks)
- since hooks are now running for client-side live preview, this means
the `lexicalHTML` field is now supported! This was a long-running issue
- this fixes a lot of population inconsistencies that we previously did
not know of. For example, it previously populated lexical and slate
relationships even if the data was saved in an incorrect format

## [Method Override
(POST)](https://payloadcms.com/docs/rest-api/overview#using-method-override-post)
change

The population request to the findByID endpoint is sent as a post
request, so that we can pass through the `data` without having to
squeeze it into the url params. To do that, it uses the
`X-Payload-HTTP-Method-Override` header.

Previously, this functionality still expected the data to be sent
through as URL search params - just passed to the body instead of the
URL. In this PR, I made it possible to pass it as JSON instead. This
means:

- the receiving endpoint will receive the data under `req.data` and is
not able to read it from the search params
- this means existing endpoints won't support this functionality unless
they also attempt to read from req.data.
- for the purpose of this PR, the findByID endpoint was modified to
support this behavior. This functionality is documented as it can be
useful for user-defined endpoints as well.

Passing data as json has the following benefits:

- it's more performant - no need to serialize and deserialize data to
search params via `qs-esm`. This is especially important here, as we are
passing large amounts of json data
- the current implementation was serializing the data incorrectly,
leading to incorrect data within nested lexical nodes

**Note for people passing their own live preview `requestHandler`:**
instead of sending a GET request to populate documents, you will now
need to send a POST request to the findByID endpoint and pass additional
headers. Additionally, you will need to send through the arguments as
JSON instead of search params and include `data` as an argument. Here is
the updated defaultRequestHandler for reference:

```ts
const defaultRequestHandler: CollectionPopulationRequestHandler = ({
  apiPath,
  data,
  endpoint,
  serverURL,
}) => {
  const url = `${serverURL}${apiPath}/${endpoint}`

  return fetch(url, {
    body: JSON.stringify(data),
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-Payload-HTTP-Method-Override': 'GET',
    },
    method: 'POST',
  })
}
```

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211124793355068
  - https://app.asana.com/0/0/1211124793355066
2025-09-05 14:40:52 -04:00
Patrik
7e98fbf78e fix(ui): cannot filter by virtual relationship fields in WhereBuilder (#13686)
### What?
- Fixed an issue where virtual relationship fields (`virtual: string`,
e.g. `post.title`) could not be used in the WhereBuilder filter
dropdown.
- Ensured regular virtual fields (`virtual: true`) are still excluded
since they are not backed by database fields.

### Why?
Previously, attempting to filter by a virtual relationship caused
runtime errors because the field was treated as a plain text field. At
the same time, non-queryable virtuals needed to remain excluded to avoid
invalid queries.

### How?
- Updated `reduceFieldsToOptions` to recognize `virtual: string` fields
and map them to their underlying path while keeping their declared type
and operators.
- Continued to filter out `virtual: true` fields in the same guard used
for hidden/disabled fields.
2025-09-04 08:17:10 -07:00