Adds `@payloadcms/typescript-plugin`, a TypeScript language service
plugin that validates PayloadComponent import paths in the IDE.
https://github.com/user-attachments/assets/2a8a8f73-a8fd-4e79-9dc4-795d817b5b75
It checks that referenced files and exports actually exist, provides
autocomplete for both file paths and export names, and supports
go-to-definition on component path strings. The plugin understands all
of Payload's path conventions including absolute paths relative to
baseDir, relative paths, tsconfig aliases, and package imports. It
auto-detects baseDir by finding the nearest payload config file and can
be overridden via tsconfig plugin options.
### What
Fix crash in JWT generation when user documents are missing group or tab
fields
### Why
When a user document was created before group/tab fields were added to
the schema, logging in would crash with "Cannot read properties of
undefined" because `getFieldsToSign` tried to traverse into undefined
fields.
### How
Added nullish coalescing (`?? {}`) to default missing group/tab data to
empty objects, consistent with how `afterRead/promise.ts` handles this
case.
Fixes#15734
### What
Adds stale data detection to prevent silent overwrites when multiple
users edit the same document.
### Why
Currently, when two users edit the same document, the last save wins
with no warning. User 1 can make changes, then User 2 saves, overwriting
User 1's work silently.
This is separate from document locking and addresses a specific
scenario:
1. Both users already have the same document open at the same time.
2. User 1 makes changes and saves.
3. User 2, still viewing their original version, then decides to make
edits.
4. Without this fix, User 2 would unknowingly overwrite User 1's changes
with no warning.
### How
Now we track the document's `updatedAt` timestamp when it initially
loads. On first edit, check if the database version has a newer
timestamp.
If stale, show a modal with the option to reload (get latest). The check
only happens once per edit session to avoid excessive DB queries.
<img width="693" height="360" alt="Screenshot 2026-02-24 at 1 37 01 PM"
src="https://github.com/user-attachments/assets/1d63973c-fa40-4213-8718-9ea739f267c9"
/>
Fixes#15486
## Summary
- Fixes `mergeLocalizedData` dropping `blockType`, `id`, and `blockName`
from non-localized blocks when the existing document has no blocks yet
(e.g. after an initial autosave before blocks were added)
- Fixes the same class of bug in `filterDataToSelectedLocales`
- Not specific to `localizeStatus: true` — affects any collection using
`publishSpecificLocale` with versioned blocks + autosave
Supersedes #15659 — the bug actually exists in **two spots**: both
`filterDataToSelectedLocales` (which #15659 addresses) **and**
`mergeLocalizedData` (which #15659 misses). This PR fixes both, using
direct property assignment instead of object spread for less overhead.
## Root cause
In both `mergeLocalizedData` and `filterDataToSelectedLocales`, the
recursive calls for non-localized blocks only iterate `block.fields`.
But `blockType`, `id`, and `blockName` are internal metadata not present
in `block.fields`. When `existingValue` is `undefined` (main doc has no
blocks because autosave only wrote to the versions table), `blockData`
falls back to `{}`, and the recursive call starts with an empty object —
metadata is lost.
When `existingValue` already has blocks, `blockData` contains
`blockType` from the spread and the bug doesn't trigger.
## Fix
After each recursive `mergeLocalizedData`/`filterDataToSelectedLocales`
call for blocks, explicitly reattach `blockType`, `id`, and `blockName`
from the incoming data. Direct property assignment (no extra object
spread) for minimal overhead.
## Related
- #15655 / #15659 — same bug class in `filterDataToSelectedLocales`
(this PR includes that fix with a more performant approach)
- #15642 / #15658 — sibling bug in `mergeLocalizedData` (unnamed groups
losing data)
## Test plan
- [x] Unit tests for `mergeLocalizedData` — 3 new tests covering empty
existing doc, undefined block field, and happy path
- [x] Unit tests for `filterDataToSelectedLocales` — new spec file with
5 tests covering block metadata preservation
- [x] Integration test — creates doc without blocks, adds blocks,
publishes with `publishSpecificLocale`, verifies `blockType` and `id`
survive
- [x] E2E test — same scenario through the admin UI + API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
Fixes#15635
Wraps `JSON.parse` in `addDataAndFileToRequest` with error handling to
return 400 Bad Request for malformed JSON bodies instead of an unhandled
500 Internal Server Error.
---------
Co-authored-by: Alessio Gravili <github@gravili.net>
### What
When saving drafts, version documents for globals weren't getting
updated `updatedAt` timestamps.
### Why
The condition in `saveVersion.ts` only checked `collection?.timestamps`,
which excluded globals even though they always have timestamps.
Broke stale data detection which relies on timestamp comparison to
detect concurrent edits.
### How
Added `|| global` check in `saveVersion.ts` since globals always have
timestamps but were excluded from the original `collection?.timestamps`
condition.
Fixes#15509
When a block used in `BlocksFeature` contains a nested `blocks` field
with `blockReferences`, the admin panel crashes with "Functions cannot
be passed directly to Client Components". This happens because the
richText schema map conversion in `buildClientFieldSchemaMap` treats all
entries as `Field` objects via an unsafe `as Field` cast. Block entries
(which have `slug` but no `type`) pass through `createClientField` which
doesn't understand them, causing `flattenedFields` - containing
server-only properties - to be copied to the client.
The fix replaces the unsafe cast with type-safe union discrimination on
the `FieldSchemaMap` value type (`Block | Field | Tab | { fields }`)
using `in` narrowing. Block entries are now routed through
`createClientBlocks` which correctly strips server-only properties,
while Field and fields-wrapper entries continue through
`createClientFields` as before.
This PR fixes the logging errors caused in CF Workers environment by
pino. Closes https://github.com/payloadcms/payload/issues/14849
It also exports `PayloadLogger` as a type to be re-used in a follow up
PR.
### What?
Prevents `req.file` from leaking between sequential
`payload.duplicate()` calls when a shared `req` object is used for
upload collections.
### Why?
When calling `payload.duplicate()` multiple times on the same `req`
object (e.g. in batch operations within a transaction), only the first
document's file was used for all subsequent duplications.
`generateFileData` writes the processed file back to `req.file`, and on
the next call `let file = req.file` was truthy, so the block that
fetches the correct source file from storage was skipped entirely.
### How?
When `isDuplicating` is true, `req.file` is now ignored so the source
file is always fetched fresh from storage for each duplicate operation.
This is a one-line change in `generateFileData.ts`:
```diff
- let file = req.file
+ let file = isDuplicating ? undefined : req.file
```
An integration test was added that creates two upload documents with
different files, duplicates both using a shared `req`, and asserts each
duplicate derives its filename from the correct source document.
Fixes#15619
### What
Adds filename + prefix sanitization to all storage adapters (S3, GCS,
Azure, R2).
### Why
Storage adapter uploads pass filenames directly to
`path.posix.join(prefix, filename)` without sanitization. Regular
uploads don't have this issue because they go through
`generateFileData`, which already sanitizes filenames via the
`sanitize-filename` package. This brings storage adapter filename
handling in line with the rest of the upload pipeline.
### How
Exported `sanitizeFilename` from `payload` to reuse in the storage
adapters.
#### Testing
Added tests to `test/storage-s3/clientUploads.int.spec.ts`
### What
Ensures unknown query operators are properly rejected during query
validation.
### Why
Previously, unrecognized operators in where clauses were silently
ignored. This could lead to unexpected behavior. Query validation should
fail-closed and only accept known operators.
### How
Added an `else` in validateQueryPaths.ts to push an error for any
operator not in the valid operator set.
Test added to `joins > int`
Blocks were previously emitted as `$ref` references in the JSON Schema.
This caused MCP tools to reject block field values during schema
validation.
Fixes#15399
### Access control can be limited to only trash
You can now limit users to only trash without being able to permanently
delete:
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
trash: true,
access: {
delete: ({ req: { user }, data }) => {
// Not logged in - no access
if (!user) {
return false
}
// Admins can do anything (trash or permanently delete)
if (user.roles?.includes('admin')) {
return true
}
// Regular users: check what operation they're attempting
// If data.deletedAt is being set, it's a trash operation - allow it
if (data?.deletedAt) {
return true
}
// Otherwise it's a permanent delete - deny for non-admins
return false
},
},
fields: [
// ...
],
}
```
---------
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
### What
Extracted duplicate regex escape logic into a shared `escapeRegExp`
utility.
### Why
The same regex escape pattern `.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')`
was duplicated across multiple locations.
### How
- Created `packages/payload/src/utilities/escapeRegExp.ts`
- Exported from payload package
- Replaced 5 inline occurrences with utility calls
This PR adds `customID` argument to `payload.db.create` which can be
used to ensure that a document is created with the provided ID, for
example:
```ts
payload.db.create({ collection: 'posts', customID: 'ce98d6c4-c3ab-45de-9dfc-bf33d94cc941', data: { } })
```
This does not require to have a custom ID field in `posts` collection,
as the following:
```ts
payload.db.create({ collection: 'posts', data: { customID: 'ce98d6c4-c3ab-45de-9dfc-bf33d94cc941' } })
```
Can be used only when you define a custom ID field
Motivation: Consistency of the local API with the `req` parameter of
requests made via the Rest / GraphQL API.
Before this change, if hooks relied on `req.query.depth`, `req: {query:
{depth: x}}` would have to be manually passed to local API calls such as
`payload.find({})`, in addition to `depth: x`. This was easy to forget,
in which case the local API calls would result in different outcomes
than if the same operation was triggered via Rest / GraphQL. This was
confusing.
---------
Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
## What
Fixes `previousValue` being `undefined` in `afterChange` hook for nested
Lexical fields.
## Why
We had this issue previously where the wrong `previousValue` data was
being accessed with nested fields, this introduced the fix of
`previousValData` which properly scopes to sibling-level or full
document data.
When the previous fix was done, the `previousValue` in the rich text
case here was missed.
## How
Changed the `richText` case in
`packages/payload/src/fields/hooks/afterChange/promise.ts` to use the
same `previousValData` pattern as other field types:
```diff
- previousValue: previousDoc?.[field.name],
+ previousValue: getNestedValue(previousValData, pathSegments) ?? previousValData?.[field.name],
```
### Testing
Added to `Lexical > lexical.int.spec.ts`
## What?
Fixes non-image files (PDF, TXT, MP3, MP4, etc.) being saved with 0
bytes when `useTempFiles: true` is enabled in the upload config.
## Why?
With `useTempFiles: true`, upload data is written to disk instead of
memory, leaving `file.data` empty. Image uploads work because Sharp
reads from `file.tempFilePath` and outputs a buffer, but non-image files
skip this and relied on `file.data`, resulting in 0-byte files being
saved.
## How?
In `generateFileData.ts` we now conditionally determine the
`bufferToSave` instead of relying on just `file.data`.
#### Testing
Test added to `payload-cloud > int`
Fixes#11832
### What?
Adds `Content-Security-Policy` headers to all `SVG` uploads, in Payload
core and storage adapters, to prevent 'SVG smuggling' (executable code
secretly embedded in the file).
### Why?
We received a report via email raising the concern that SVG files
containing embedded JavaScript could be uploaded and executed in the
admin panel.
Common approaches in similar frameworks are:
- Blocking SVG uploads entirely, which is highly restrictive
- Leaving enforcement to the user, requires full manual configuration
Payload already includes SVG validation via `validateSvg()` which strips
known malicious patterns. This change adds the CSP header as an
additional layer of defense. It isn’t strictly required, but it is a
simple addition that provides extra protection and has no real downside,
beyond this, handling and validating SVGs remains the user’s
responsibility.
**Note**: Users who need to serve interactive SVGs can override this
behavior using the `modifyResponseHeaders` option in their upload
collection config.
### How?
- Detects SVGs and conditionally adds CSP header in `uploads/getFile.ts`
- Same logic added to all storage adapter `staticHandler.ts` files
#### Testing
Added to `uploads > int`
#### Acknowledgement
Reported by: Dominic Breuker @ gematik
Relies on https://github.com/payloadcms/payload/pull/15405 first
When using a storage adapter such as S3 to handle files there were
errors in the way the files would be fetched for imports.
This PR fixes that, adding test coverage and changes the job behaviour
so that it only stores the import doc ID and not the full file data -
this avoids many problems down the line and optimises memory and storage
usage.
Fixes https://github.com/payloadcms/payload/issues/15247
Previously attempting to use an import with the jobs queue with S3
storage would result in this error:
```text
[11:46:25] ERROR: Failed to queue import job for document 698628f19aaca7d4f4f18c6e
err: {
"type": "Error",
"message": "ENOENT: no such file or directory, open '/posts-with-s3-import/2026-02-06 114542.csv'",
"stack":
Error: ENOENT: no such file or directory, open '/posts-with-s3-import/2026-02-06 114542.csv'
```
Also:
- Exports `getExternalFile` utility from `payload/internals`
The Vitest-only branch in `dynamicImport` uses a bare
`import(importPath)` which webpack still statically analyzes, even
though it's behind a `process.env.VITEST` condition. This could cause a
`"Critical dependency: the request of a dependency is an expression"`
warning in Next.js projects. Adding `/* webpackIgnore: true */` should
fix this.
Additionally, this hides another sass deprecation warning
### What?
This PR fixes `tempFileDir` configuration not respecting absolute paths
when `useTempFiles` is enabled for uploads.
### Why?
1. **Absolute paths ignored**: Setting `tempFileDir: '/tmp'` would still
create temp files under the working directory (e.g.,
`/Users/project/tmp/...`) instead of `/tmp/...`
2. **Path duplication**: The default `tempFileDir` could result in
duplicated working directory segments in the path
### How?
Uses `path.resolve()` to handle both cases:
- Absolute paths (e.g., `/tmp`) are returned as-is
- Relative paths (e.g., `tmp`) are resolved against `process.cwd()`
Fixes#12910
---------
Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
### What?
Fixes image sizes storing the original document URL in the database when
the size could not be generated (should be `null` instead).
### Why?
When an uploaded image is smaller than a defined image size (or cannot
be resized, e.g., SVG), Payload correctly sets `filename`, `width`,
`height`, `mimeType`, and `filesize` to `null`. However, the `url` field
was incorrectly falling back to the original image URL.
For background, there was a previous issue where the URL for generated
sizes was always being set to the main URL. This was resolved, but still
left the main URL as a fallback - really this should have been
completely removed.
### How?
Removed the fallback to the original filename in the `beforeChange` hook
for image size URL fields. Now when a size cannot be generated, the URL
is `null` (consistent with the other size fields).
Fixes: #15450
---------
Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
Allows custom components to be slotted in before the nav, and not
specifically beforeNavLinks. `beforeNavLinks` semantically means before
the links themselves, but for a PR like
https://github.com/payloadcms/payload/pull/15470, we will want to be
able to place things outside of the nav links section. Think about the
multi-tenant selector, you would not want that visible in just the first
tab, but outside of the tabs - i.e. before the nav.
Replaces hardcoded preference key strings with centralized constants to
prevent typos and improve maintainability. Creates PREFERENCE_KEYS
object in packages/payload/src/preferences/keys.ts and updates all
usages throughout the codebase.
I have another PR that sets preferences and figured it would be nice if
we had a centralized place for the pref constants.
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
### What
Fixes an `uploads` related security issue where redirects are able
bypass URL validation in external file uploads.
### Why
When uploading files via an external URL, HTTP redirects (3xx responses)
were automatically followed. This meant an potential attacker could
provide a public URL that redirected to internal services, bypassing the
security checks that only ran on the initial URL.
### How
- Added `redirect: 'manual'` to `safeFetch()` to prevent automatic
redirect following
- Modified `getExternalFile()` to manually handle redirects with
re-validation at each step
- Limited redirects to maximum of 3x to prevent infinite loops
- Tests added to `uploads` test suite
#### Acknowledgement
Big thank you to `r3dbrothers1` for the responsible disclosure.
Requires https://github.com/payloadcms/payload/pull/15427 to be merged
first.
This PR avoids re-calculating permissions in certain server functions,
since through #15427, they are now available in server functions due to
an `initReq` that we already ran.
It also passes some missing properties to `serverProps` and render
handles that are now available.
This PR ensures that all documents returned from auth-enabled
collections via local API operations include the `collection` property,
which is essential for multi-tenant access control to function
correctly.
## Motivation
A common pattern in Payload applications is to fetch a user from the
database and pass it to subsequent operations:
```typescript
const sean = (await payload.find({
collection: 'users',
where: { email: { equals: 'sean@payloadcms.com' } }
})).docs[0]
const result = await payload.find({
collection: 'diaries',
overrideAccess: false,
user: sean, // pass fetched user
})
// result.docs[0]: { title: "sean's diary" }
// result.docs[1]: { title: "jarrod's journal" } // Oh no! This is private!
```
This introduces a security vulnerability. The local API expects
`user.collection` to be present, but user documents returned by
`find`/`findByID` did not include this property. Since the `user`
parameter is loosely typed, TypeScript does not catch this error.
In the `@payloadcms/plugin-multi-tenant` plugin, `useTenantAccess`
checks `req.user.collection` to apply tenant-based access control. This
is not an issue within plugin-multi-tenant, because the Payload types
mark `req.user.collection` as required - thus it's reasonable to expect
this property to be there. When `collection` is `undefined`, the
condition `args.req.user.collection === adminUsersSlug` always evaluates
to `false`, bypassing tenant access restrictions.
## Changes
The `collection` property is now injected into auth collection documents
immediately after data is fetched from the database in all collection
operations (`find`, `findByID`, `create`, `update`, `delete`, etc.).
This makes it available throughout all operation hooks.
Type generation has been updated to include `collection` directly in
auth collection schemas, producing cleaner types where `User` contains
`collection: 'users'` without intersection types. Input types have been
updated to make `collection` optional since it's auto-populated.
As a fallback, `createLocalReq` now sets `user.collection` to the
default admin user collection if missing - this is an additional
safeguard. A TODO has been added to throw an error instead in version
4.0. We cannot throw an error now, as this would be a breaking change.
[[Link to internal Slack
discussion]](https://payloadcms.slack.com/archives/C049BR3QBHC/p1769593583792739)
Currently, we only pass `importMap` and `req` to all server functions.
This PR extends that interface by passing the following **additional
arguments** to every server function and every dashboard widget:
- `cookies`
- `locale`
- `permissions`
This change comes at **no extra cost**, since all of these values are
already available via `initReq`, which we are already calling.
## Why?
I’m working on rendering the list view in a dashboard widget through a
server function, which requires context such as `permissions`.
Recomputing these values would be redundant and inefficient, given that
they’ve already been calculated in `handleServerFunctions` and we can
just pass them through.
By passing them through once, we avoid duplicate work and make server
functions easier to use.
### What
Fixes preferences being accessible across different auth collections in
multi-auth setups.
### Why
The `preferenceAccess` function only checked user ID without verifying
which auth collection the user belonged to. In setups with multiple auth
collections using sequential IDs, this could allow unintended access to
preferences.
### How
Updated `preferenceAccess` to check both user ID and auth collection,
consistent with the existing operation handlers. Added tests to verify
proper isolation.
## What
This PR extends the `typescript.strictDraftTypes` configuration flag to
enforce **draft type safety across all Local API operations** that
accept a `draft` parameter, for both collections and globals.
When `strictDraftTypes: true` is enabled, the `draft` option is now:
- ❌ **Forbidden** for collections/globals without drafts enabled
- ✅ **Allowed** for collections/globals with drafts enabled
- ⚠️ **Conditionally allowed** when the slug is generic and cannot be
statically narrowed
Additionally, in **create operations**:
- `DraftDataFromCollectionSlug` can only be used with **draft-enabled**
collections
- This prevents sending partial draft data to non-draft collections
---
## ⚠️ Important Note on Generic Usage
This change may surface backward-compatibility issues when **generic
collection or global slugs** are used.
When a Local API helper accepts a generic `TSlug`, TypeScript may be
unable to determine whether drafts are enabled. In these cases, an
explicit type assertion may be required:
```ts
function createDocument<TSlug extends CollectionSlug>(slug: TSlug) {
return payload.create({
collection: slug,
data: someData,
draft: true,
} as Options<TSlug, SelectType>)
}
```
This is a limitation of TypeScript’s type narrowing and does not affect
runtime behavior.
## Why
PR #14388 introduced the `strictDraftTypes` flag to improve **query
return types** when `draft: true` is used (for example, making required
fields optional).
However, draft usage itself was not enforced, which led to several
issues:
1. **Silent runtime behavior**
Passing `draft: true` to a collection or global without drafts enabled
is silently ignored
2. **Type safety gaps**
TypeScript allows partial draft data to be sent to entities that do not
support drafts
3. **Inconsistent API behavior**
Some operations enforced draft rules, while others did not
This PR addresses these gaps by enforcing draft constraints at **compile
time**, ensuring invalid draft usage is caught during development.
## How
### 1. Introduced reusable helper types
- `CollectionsWithoutDrafts` / `GlobalsWithoutDrafts`
Identify collections and globals that do not support drafts
- `DraftFlagFromCollectionSlug<TSlug>` /
`DraftFlagFromGlobalSlug<TSlug>`
Conditional types that:
- Respect the `strictDraftTypes` flag
- Forbid the `draft` property for non-draft entities
- Allow the `draft` property for draft-enabled or generic slugs
### 2. Applied helpers across all operations
- Split operation `Options` types into `BaseOptions` plus draft-flag
intersections
- Removed duplicated draft-related type logic across files
### 3. Documentation updates
- Expanded the `strictDraftTypes` documentation to cover draft
enforcement across **all Local API operations**
---
## Backward Compatibility
- The feature is **opt-in**
- `strictDraftTypes` defaults to `false`
- Existing code is unaffected unless the flag is enabled
---
## Related
Builds upon **#14388**, which introduced `strictDraftTypes` for query
return types.
---------
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
Exports the `getSafeFileName` utility from `payload/internal` so we can
reuse it in the storage adapters for client side uploads (there's a bug
fix in a subsequent PR).
Adds some unit tests for this util
### What
Adds the ability to customize the `UnpublishButton` component in
collection and global configs, following the same pattern as
`PublishButton`.
### Why
`UnpublishButton` was hardcoded while other document control buttons
(`PublishButton`, `SaveButton`, etc.) could be customized.
### How
Added type definitions, config options, component resolution in
renderDocumentSlots, and updated DocumentControls to use the
RenderCustomComponent pattern.
---------
Co-authored-by: Alessio Gravili <github@gravili.net>
### What?
The `traverseFields` function returns the wrong dot notation for the
`parentPath` argument in `callback` for non-localized tab fields.
### Why?
Dot is missing in the template string.
### How?
Added the missing dot and a unit test for this case. I can extend the
test to cover more of the `traverseFields` function if needed.
### Before
```ts
{
type: 'tabs',
tabs: [
{
label: 'Tab 1',
name: 'tab1',
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Ui Only Tab',
fields: [
{
type: 'group',
name: 'group1',
label: 'Group 1',
fields: [
{
type: 'text',
name: 'text1',
label: 'Text 1',
},
],
},
],
},
],
},
],
},
],
}
```
The `parentPath` argument in `callback` returns as `tab1group1.`.
### After
Now returns `parentPath` as `tab1.group1.` as expected.
### What
Re-exports UI components from `@payloadcms/next/rsc` and
`@payloadcms/next/client` instead of requiring direct imports from
`@payloadcms/ui` in the generated importMap.
### Why
The generated `importMap.js` was importing from `@payloadcms/ui`, which
isn't in users' `package.json` (it's a transitive dependency through
`@payloadcms/next`). This breaks import resolution with pnpm and can
fail in production builds with any package manager without
`@payloadcms/ui` installed.
### How
- Added re-exports in `@payloadcms/next/rsc` for: `CollectionCards`,
`FolderField`, `FolderTableCell`, `SlugField`
- Added re-exports in `@payloadcms/next/client` for: `FolderTypeField`
and all `QueryPresets*` components
- Updated all component paths in `@payloadcms/payload` to use
`@payloadcms/next/rsc` or `@payloadcms/next/client` instead of
`@payloadcms/ui`
Fixes#15010
## Issue
Fixes `find` afterRead operations that should behave like `findByID`
afterRead hooks do.
## Before
The find afterRead hooks were falling back to the `doc` if the hook
returned undefined/null.
## After
The find afterRead hooks now fallback to the `docRef` if the afterRead
hook returns undefined/null.
## Alignment
You can see
[here](https://github.com/payloadcms/payload/blob/main/packages/payload/src/collections/operations/findByID.ts#L310-L319)
that the findByID already behaves this way.