mirror of
https://github.com/payloadcms/payload.git
synced 2026-09-14 20:07:19 +08:00
api-key-field-access
81 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5081ad4786 |
feat: add TanStack Start framework adapter (#16139)
Adds `@payloadcms/tanstack-start` — the first non-Next.js framework adapter for Payload's admin panel. ## Background The framework adapter pattern already landed across four PRs: - [Server adapter](#16753) - [Router adapter](#16763) - [View adapter](#16803) - [Layout adapter](#16840) This pushed every Next-specific concern (routing, request init, server functions, HMR) behind typed contracts in `payload` and made `@payloadcms/ui` framework-agnostic. This PR is the payoff: a working adapter built entirely on that abstraction, proving the admin panel renders on a non-Next stack with no forks of the UI. ## Motivation - **Prove the abstraction.** The adapter contracts are only as good as a second implementation. TanStack Start exercises every seam — a different renderer, a different server-function transport, a different build tool — and validates that `@payloadcms/ui` carries no hidden Next.js assumptions. - **Meet users where they are.** Not every project is on Next.js. Decoupling the admin panel opens Payload to the broader React SSR ecosystem, with TanStack Start as the first proof point. - **Keep one UI.** Both adapters render the same `@payloadcms/ui` components and data fetchers. There is no TanStack fork of the admin panel — only a thin adapter package that satisfies the contracts. ## How it differs from the Next.js adapter Same UI, different plumbing behind the contracts: | | Next.js | TanStack Start | | ---------------- | -------------------------- | --------------------------------- | | Server rendering | RSC flight payloads | SSR + route loaders | | Server functions | `'use server'` actions | `createServerFn` | | Request init | `next/headers` | `@tanstack/react-start/server` | | Build / HMR | Webpack / Turbopack | Vite | ## Integration touch points Wiring Payload into a TanStack Start app is a handful of file routes, similar to the Next.js app dir shipped since Payload v3: ``` app/ ├── __root.tsx # root shell — withPayloadRoot swaps in the admin document on /admin ├── _frontend.tsx # your app's layout route ├── _frontend/ # your app's routes ├── _payload.tsx # admin layout route — mounts Payload providers └── _payload/ ├── admin.index.tsx # /admin ├── admin.$.tsx # /admin/* (splat) ├── api.$.ts # /api/* — Payload REST handlers └── server.functions.ts # config + importMap injection; server functions ``` **Root shell** — This is the highest level touchpoint that affects your app. In your root route file, add the `withPayloadRoot` shell component: ```tsx // app/__root.tsx import { withPayloadRoot } from "@payloadcms/tanstack-start/client"; export const Route = createRootRoute({ shellComponent: withPayloadRoot(MarketingRoot), }); ``` For all other file contents, see the `app-tanstack` directory in the monorepo (subject to change). Docs to be provided in the future. ## Status Experimental. Ships as a new package alongside the Next.js adapter; nothing in the existing Next path changes at runtime. --------- Co-authored-by: Jake Fletcher <jacobsfletch@gmail.com> |
||
|
|
b77493d41a |
feat!: replace TypedUser with User and add AuthenticatedUser (#17151)
## What
This completes the user-type cleanup planned for Payload 4.0:
- `UntypedUser` was deprecated for removal in 4.0.
- `TypedUser` was marked to be renamed to `User` in 4.0.
Previously, the public `User` type was the loose, deprecated
`UntypedUser`, while `TypedUser` was the generated type for auth-enabled
collections. `ClientUser` was also loose, and `req.user` did not include
the runtime auth fields `_strategy` and `_sid`.
The types now have clear roles:
| Type | Purpose |
| --- | --- |
| `User` | The generated user type for auth-enabled collections. This
replaces `TypedUser`. Without generated types, it falls back to a
documented shape containing Payload's built-in auth fields. Contains
both read and write fields. |
| `AuthenticatedUser` | `User` plus the optional runtime fields
`_strategy` and `_sid`. Used by `PayloadRequest.user`, `payload.auth()`,
auth strategy results, and auth internals. |
| `ClientUser` | The type used by `useAuth().user` and `me` responses.
It is now an alias of `AuthenticatedUser` |
I'm considering replacing `ClientUser` in favor of just
`AuthenticatedUser` in a separate PR.
## Breaking changes
- `TypedUser` has been removed. Use `User`.
- `UntypedUser` has been removed. Use `User` for a user document,
`AuthenticatedUser` for a signed-in request user, or `ClientUser` in
client code.
- `User` and `ClientUser` no longer have an `[key: string]: any` index
signature. Custom auth-collection fields require generated types or an
explicit augmented type.
- The Local API `user` option is now `User | null` instead of the loose
`Document` type for:
- collection `count`, `create`, `delete`, `duplicate`, `find`,
`findByID`, `findDistinct`, and `update`
- collection and global version count, find, find-by-ID, and restore
operations
- global `findOne` and `update`
- `UserSession.createdAt` is now optional and nullable: `createdAt?:
Date | null | string`. This matches generated session types, but callers
must handle a missing value.
`AuthenticatedUser` is assignable to `User`, so passing `req.user` to
these Local API operations continues to work.
## Other changes
- Adds a strict untyped fallback containing all built-in user fields,
without an index signature. A type test verifies that generated user
types are assignable to this fallback.
- Types `payload.auth()` and login results with the runtime auth fields,
and uses `AuthenticatedUser` while login, `me`, refresh, and session
code build signed-in users.
- Fixes the session lookup ID type and updates session handling for
nullable `createdAt` values.
- Hardens refresh/session handling when a user or session is missing and
avoids mutating the in-memory user's `updatedAt` solely to control
database timestamps.
- Updates Payload, the admin UI, Lexical, tests, and first-party plugins
to use the new types:
- `plugin-mcp` uses `User` for the authorized caller.
- `plugin-import-export` removes obsolete `req.user.user` handling
- `plugin-multi-tenant` explicitly casts accesses to plugin-defined user
fields.
- `plugin-ecommerce` adds `UserWithCart` for the optional reverse `cart`
join that projects may
define on their user collection.
## Migration
```diff
- import type { TypedUser, UntypedUser } from 'payload'
+ import type { User } from 'payload'
```
Use `User` for stored/read user documents and `AuthenticatedUser` when
code specifically receives the signed-in user from `req.user`,
`payload.auth()`, or an auth strategy.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1215866573673868
|
||
|
|
6fe43ba2d6 |
feat(plugin-mcp)!: redesign API key management (#16986)
This PR improves MCP API key management in the admin panel. ## Admin redesign MCP API keys move out of the main nav and into the user menu under **Settings → Manage API keys**. <img width="2962" height="2070" alt="screenshot 2026-06-14 at 18 52 33@2x" src="https://github.com/user-attachments/assets/7c9bbdb0-4c6b-43af-8799-2bc352e633c9" /> <img width="858" height="606" alt="screenshot 2026-06-12 at 14 46 02@2x" src="https://github.com/user-attachments/assets/79721def-7c2f-4fbc-8628-96019b45d03c" /> <img width="2416" height="634" alt="screenshot 2026-06-12 at 14 46 30@2x" src="https://github.com/user-attachments/assets/e2b502c7-d160-464b-aa6f-c1dd26b25bef" /> ## API key collection The MCP API key collection is no longer auth-enabled. MCP keys still belong to a Payload user, but the key collection itself is no longer treated like a login-capable user collection. That keeps generated auth user types cleaner. ## Migration Existing MCP API keys should be recreated after upgrading. The collection slug is unchanged, but the API key fields and access settings changed. The public `baseAPIKeyFields` export was replaced by `createAPIKeyFields()`. |
||
|
|
f2ea90a1fa |
fix(ui): respect admin.condition on row fields (#16981)
Same as https://github.com/payloadcms/payload/pull/16954, applied to 4.x. Rows aren't respecting their admin.condition property. This is because `path` was never forwarded to the row field, so its `withCondition` wrapper looked up `passesCondition` at an undefined path. The row fields never become hidden no matter what the `admin.condition` evaluates to. ## Before https://github.com/user-attachments/assets/3b859409-16ba-42c8-91ad-f82fca01ccd9 ## After https://github.com/user-attachments/assets/30cd5edc-f32c-447f-b74a-0cd95d13af2c Co-authored-by: Amelia <44613453+LimChorngUan@users.noreply.github.com> |
||
|
|
7a3f43ff51 |
fix: prevent silent data overwrites on concurrent edits (#15749)
### 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 |
||
|
|
0a123b51f2 |
feat(next, ui): widget fields (#15700)
## Summary
- Adds `fields` support to dashboard widgets, analogous to how Blocks
work — widgets can now declare configurable fields
- Widget data is editable from a new drawer UI (edit icon) when in
dashboard editing mode
- Full type generation: `WidgetInstance<T>` is now generic with typed
`data` and `width` based on the widget's field schema and min/max width
constraints
- `WidgetServerProps` is generic so widget components receive typed
`widgetData`
## Usage
```ts
import { buildConfig } from 'payload'
export default buildConfig({
admin: {
dashboard: {
widgets: [
{
slug: 'sales-summary',
ComponentPath: './components/SalesSummary.tsx#default',
fields: [
{ name: 'title', type: 'text' },
{
name: 'timeframe',
type: 'select',
options: ['daily', 'weekly', 'monthly', 'yearly'],
},
{ name: 'showTrend', type: 'checkbox' },
],
minWidth: 'small',
maxWidth: 'medium',
},
],
},
},
})
```
```tsx
import type { WidgetServerProps } from 'payload'
import type { SalesSummaryWidget } from '../payload-types'
export default async function SalesSummaryWidgetComponent({
widgetData,
}: WidgetServerProps<SalesSummaryWidget>) {
const title = widgetData?.title ?? 'Sales Summary'
const timeframe = widgetData?.timeframe ?? 'monthly'
return (
<div className="card">
<h3>
{title} ({timeframe})
</h3>
</div>
)
}
```
## Demo
https://github.com/user-attachments/assets/b24d3635-8635-4d5f-84af-a6dcf801aa4f
|
||
|
|
d288752f68 |
fix: field schema map paths (#10852)
Continuation of #10638. Field paths within the schema map are not correct. For example, an unnamed tab containing a text field should have the schema path: - `_index-0.fieldWithinUnnamedTab` However, within the schema map that path is formatted as: - `fieldWithinUnnamedTab` The leading index in the first example _should_ exist, as this field is nested within another field, regardless of this field being unnamed. Otherwise, it would be impossible to traverse the schema and lookup this field. This discrepancy is not an issue in the admin panel because fields are _also_ provided the wrong schema paths. They can properly locate their schema within the schema map because they match despite being incorrect. Both are wrong and that's why it works. Here's comprehensive example of how field paths _should_ be formatted: ```ts { // ... fields: [ { // path: 'topLevelNamedField' // schemaPath: 'topLevelNamedField' // indexPath: '' name: 'topLevelNamedField', type: 'text', }, { // path: 'array' // schemaPath: 'array' // indexPath: '' name: 'array', type: 'array', fields: [ { // path: 'array.[n].fieldWithinArray' // schemaPath: 'array.fieldWithinArray' // indexPath: '' name: 'fieldWithinArray', type: 'text', }, { // path: 'array.[n].nestedArray' // schemaPath: 'array.nestedArray' // indexPath: '' name: 'nestedArray', type: 'array', fields: [ { // path: 'array.[n].nestedArray.[n].fieldWithinNestedArray' // schemaPath: 'array.nestedArray.fieldWithinNestedArray' // indexPath: '' name: 'fieldWithinNestedArray', type: 'text', }, ], }, { // path: 'array.[n]._index-2' // schemaPath: 'array._index-2' // indexPath: '2' type: 'row', fields: [ { // path: 'array.[n].fieldWithinRowWithinArray' // schemaPath: 'array._index-2.fieldWithinRowWithinArray' // indexPath: '' name: 'fieldWithinRowWithinArray', type: 'text', }, ], }, ], }, { // path: '_index-2' // schemaPath: '_index-2' // indexPath: '2' type: 'row', fields: [ { // path: 'fieldWithinRow' // schemaPath: '_index-2.fieldWithinRow' // indexPath: '' name: 'fieldWithinRow', type: 'text', }, ], }, { // path: '_index-3' // schemaPath: '_index-3' // indexPath: '3' type: 'tabs', tabs: [ { // path: '_index-3-0' // schemaPath: '_index-3-0' // indexPath: '3-0' label: 'Unnamed Tab', fields: [ { // path: 'fieldWithinUnnamedTab' // schemaPath: '_index-3-0.fieldWithinUnnamedTab' // indexPath: '' name: 'fieldWithinUnnamedTab', type: 'text', }, { // path: '_index-3-0-1' // schemaPath: '_index-3-0-1' // indexPath: '3-0-1' type: 'tabs', tabs: [ { // path: '_index-3-0-1-0' // schemaPath: '_index-3-0-1-0' // indexPath: '3-0-1-0' label: 'Nested Unnamed Tab', fields: [ { // path: 'fieldWithinNestedUnnamedTab' // schemaPath: '_index-3-0-1-0.fieldWithinNestedUnnamedTab' // indexPath: '' name: 'fieldWithinNestedUnnamedTab', type: 'text', }, ], }, ], }, ], }, { // path: 'namedTab' // schemaPath: '_index-3.namedTab' // indexPath: '' label: 'Named Tab', name: 'namedTab', fields: [ { // path: 'namedTab.fieldWithinNamedTab' // schemaPath: '_index-3.namedTab.fieldWithinNamedTab' // indexPath: '' name: 'fieldWithinNamedTab', type: 'text', }, ], }, ], }, ] } ``` |
||
|
|
ad0e7b2252 |
fix(ui): preview button not responding to conditional URL (#14277)
Fixes #14241 and #11253. Preview URL is now part of the form state as well so we can conditionally show the preview button based on its value, previously this was not possible because we were fetching the URL on click. Similar to #14012. |
||
|
|
b09ae6772f |
feat: slug field (#14007)
Discussion #8859. Requires #14012. Exports a new `slugField`. This is a wrapper around the text field that you can drop into any field schema. A slug is a unique, indexed, URL-friendly string that identifies a particular document, often used to construct the URL of a webpage. Slugs are a fundamental concept for seemingly every project. Traditionally, you'd build this field from scratch, but there are many edge cases and nice-to-haves to makes this difficult to maintain by hand. For example, it needs to automatically generate based on the value of another field, provide UI to lock and re-generate the slug on-demand, etc. Fixes #13938. When autosave is enabled, the slug is only ever generated once after the initial create, leading to single character, or incomplete slugs. For example, it is expected that "My Title" → "my-title, however ends up as "m". This PR overhauls the field to feel a lot more natural. Now, we only generate the slug through: 1. The `create` operation, unless the user has modified the slug manually 2. The `update` operation, if: a. Autosave is _not_ enabled and there is no slug b. Autosave _is_ enabled, the doc is unpublished, and the user has not modified the slug manually The slug should stabilize after all above criteria have been met, because the URL is typically derived from the slug. This is to protect modifying potentially live URLs, breaking links, etc. without explicit intent. This fix, along with all the other features, is now standardized behind the new `slugField`: ```ts import type { CollectionConfig } from 'payload' import { slugField } from 'payload' export const MyCollection: CollectionConfig = { // ... fields: [ // ... slugField() ] } ``` In the future we could also make this field smart enough to auto increment itself when its generated slug is not unique. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211513433305005 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
1e13474068 |
fix: deeply merge array and block rows from server form state (#13551)
Continuation of https://github.com/payloadcms/payload/pull/13501. When merging server form state with `acceptValues: true`, like on submit (not autosave), rows are not deeply merged causing custom row components, like row labels, to disappear. This is because we never attach components to the form state response unless it has re-rendered server-side, so unless we merge these rows with the current state, we lose them. Instead of allowing `acceptValues` to override all local changes to rows, we need to flag any newly added rows with `addedByServer` so they can bypass the merge strategy. Existing rows would continue to be merged as expected, and new rows are simply appended to the end. Discovered here: https://discord.com/channels/967097582721572934/967097582721572937/1408367321797365840 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211115023863814 |
||
|
|
0b60bf2eff | fix(ui): significantly more predictable autosave form state (#13460) | ||
|
|
c5c8c13057 |
fix(next): pass req through document tab conditions and custom server components (#13302)
Custom document tab components (server components) do not receive the `user` prop, as the types suggest. This makes it difficult to wire up conditional rendering based on the user. This is because tab conditions don't receive a user argument either, forcing you to render the default tab component yourself—but a custom component should not be needed for this in the first place. Now they both receive `req` alongside `user`, which is more closely aligned with custom field components. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210906078627357 |
||
|
|
f63dc2a10c |
feat: adds trash support (soft deletes) (#12656)
### What?
This PR introduces complete trash (soft-delete) support. When a
collection is configured with `trash: true`, documents can now be
soft-deleted and restored via both the API and the admin panel.
```
import type { CollectionConfig } from 'payload'
const Posts: CollectionConfig = {
slug: 'posts',
trash: true, // <-- New collection config prop @default false
fields: [
{
name: 'title',
type: 'text',
},
// other fields...
],
}
```
### Why
Soft deletes allow developers and admins to safely remove documents
without losing data immediately. This enables workflows like reversible
deletions, trash views, and auditing—while preserving compatibility with
drafts, autosave, and version history.
### How?
#### Backend
- Adds new `trash: true` config option to collections.
- When enabled:
- A `deletedAt` timestamp is conditionally injected into the schema.
- Soft deletion is performed by setting `deletedAt` instead of removing
the document from the database.
- Extends all relevant API operations (`find`, `findByID`, `update`,
`delete`, `versions`, etc.) to support a new `trash` param:
- `trash: false` → excludes trashed documents (default)
- `trash: true` → includes both trashed and non-trashed documents
- To query **only trashed** documents: use `trash: true` with a `where`
clause like `{ deletedAt: { exists: true } }`
- Enforces delete access control before allowing a soft delete via
update or updateByID.
- Disables version restoring on trashed documents (must be restored
first).
#### Admin Panel
- Adds a dedicated **Trash view**: `/collections/:collectionSlug/trash`
- Default delete action now soft-deletes documents when `trash: true` is
set.
- **Delete confirmation modal** includes a checkbox to permanently
delete instead.
- Trashed documents:
- Displays UI banner for better clarity of trashed document edit view vs
non-trashed document edit view
- Render in a read-only edit view
- Still allow access to **Preview**, **API**, and **Versions** tabs
- Updated Status component:
- Displays “Previously published” or “Previously a draft” for trashed
documents.
- Disables status-changing actions when documents are in trash.
- Adds new **Restore** bulk action to clear the `deletedAt` timestamp.
- New `Restore` and `Permanently Delete` buttons for
single-trashed-document restore and permanent deletion.
- **Restore confirmation modal** includes a checkbox to restore as
`published`, defaults to `draft`.
- Adds **Empty Trash** and **Delete permanently** bulk actions.
#### Notes
- This feature is completely opt-in. Collections without trash: true
behave exactly as before.
https://github.com/user-attachments/assets/00b83f8a-0442-441e-a89e-d5dc1f49dd37
|
||
|
|
4458f74cef |
ci: template errors not being caught due. fix: error due to updated generated-types User type (#12973)
This PR consists of two separate changes. One change cannot pass CI without the other, so both are included in this single PR. ## CI - ensure types are generated Our website template is currently failing to build due to a type error. This error was introduced by a change in our generated types. Our CI did not catch this issue because it wasn't generating types / import map before attempting to build the templates. This PR updates the CI to generate types first. It also updates some CI step names for improved clarity. ## Fix: type error  This fixes the type error by ensuring we consistently use the _same_ generated `TypedUser` object within payload, instead of `BaseUser`. Previously, we sometimes used the generated-types user and sometimes the base user, which was causing type conflicts depending on what the generated user type was. It also deprecates the `User` type (which was essentially just `BaseUser`), as consumers should use `TypedUser` instead. `TypedUser` will automatically fall back to `BaseUser` if no generated types exists, but will accept passing it a generated-types User. Without this change, additional properties added to the user via generated-types may cause the user object to not be accepted by functions that only accept a `User` instead of a `TypedUser`, which is what failed here. ## Templates: re-generate templates to update generated types --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210668927737258 |
||
|
|
4e2e4d2aed |
feat(next): version view overhaul (#12027)
#11769 improved the lexical version view diff component. This PR improves the rest of the version view. ## What changed - Column layout when selecting a version: - Previously: Selected version on the left, latest version on the left - Now: Previous version on the left, previous version on the right (mimics behavior of GitHub) - Locale selector now displayed in pill selector, rather than react-select - Smoother, more reliable locale, modifiedOnly and version selection. Now uses clean event callbacks rather than useEffects - React-diff-viewer-continued has been replaced with the html differ we use in lexical - Updated Design for all field diffs - Version columns now have a clearly defined separator line - Fixed collapsibles showing in version view despite having no modified fields if modifiedOnly is true - New, redesigned header ## Screenshots ### Before   ### After     |
||
|
|
48218bccb5 |
chore: fix lint warnings for default exports, unused imports, unused err in catch (#12666)
Fix various lint warnings in payload package. 387 warnings -> 215 warnings - Migrate (most) default exports to named - Remove unused imports - Rename unused errors in catch statements to `ignore` |
||
|
|
6ec21a53ff |
chore: migrate to TypeScript strict in Payload package (enable strictNullChecks) - #3 (#12586)
Important: An intentional effort is being made during migration to not modify runtime behavior. This implies that there will be several assertions, non-null assertions, and @ts-expect-error. This philosophy applies only to migrating old code to TypeScript strict, not to writing new code. For a more detailed justification for this reasoning, https://github.com/payloadcms/payload/pull/11840#discussion_r2021975897. In this PR, instead of following the approach of migrating a subset of files, I'm migrating all files by disabling a specific rule. In this case, `strictNullChecks`. `strictNullChecks` is a good rule to start the migration with because it's easy to silence with non-null assertions or optional chainings. Additionally, almost all ts strict errors are due to this rule. This PR improves 200+ files, leaving only 68 remaining to migrate to strict mode in the payload package. |
||
|
|
f75d62c79b |
feat: select field filter options (#12487)
It is a common pattern to dynamically show and validate a select field's
options based on various criteria such as the current user or underlying
document.
Some examples of this might include:
- Restricting options based on a user's role, e.g. admin-only options
- Displaying different options based on the value of another field, e.g.
a city/state selector
While this is already possible to do with a custom `validate` function,
the user can still view and select the forbidden option...unless you
_also_ wired up a custom component.
Now, you can define `filterOptions` on select fields.
This behaves similarly to the existing `filterOptions` property on
relationship and upload fields, except the return value of this function
is simply an array of options, not a query constraint. The result of
this function will determine what is shown to the user and what is
validated on the server.
Here's an example:
```ts
{
name: 'select',
type: 'select',
options: [
{
label: 'One',
value: 'one',
},
{
label: 'Two',
value: 'two',
},
{
label: 'Three',
value: 'three',
},
],
filterOptions: ({ options, data }) =>
data.disallowOption1
? options.filter(
(option) => (typeof option === 'string' ? options : option.value) !== 'one',
)
: options,
}
```
|
||
|
|
d9c0c43154 |
fix(ui): passes value to server component args (#12352)
### What? Allows the field value (if defined) to be accessed from `args` with custom server components. ### Why? Documentation states that the user can access `args.value` to get the value of the field at time of render (if a value is defined) when using a custom server component - however this isn't currently setup. <img width="469" alt="Screenshot 2025-05-08 at 4 51 30 PM" src="https://github.com/user-attachments/assets/9c167f80-5c5e-4fea-a31c-166281d9f7db" /> Link to docs [here](https://payloadcms.com/docs/fields/overview#default-props). ### How? Passes the value from `data` if it exists (does not exist for all field types) and adds `value` to the server component types as an optional property. Fixes #10389 |
||
|
|
4d7c1d45fa |
fix(ui): form state race conditions (#12026)
Fixes form state race conditions. Modifying state while a request is in
flight or while the response is being processed could result in those
changes being overridden.
This was happening for a few reasons:
1. Our merge logic was incorrect. We were disregarding local changes to
state that may have occurred while form state requests are pending. This
was because we were iterating over local state, then while building up
new state, we were ignoring any fields that did not exist in the server
response, like this:
```ts
for (const [path, newFieldState] of Object.entries(existingState)) {
if (!incomingState[path]) {
continue
}
// ...
}
```
To fix this, we need to use local state as the source of truth. Then
when the server state arrives, we need to iterate over _that_. If a
field matches in local state, merge in any new properties. This will
ensure all changes to the underlying state are preserved, including any
potential addition or deletions.
However, this logic breaks down if the server might have created _new_
fields, like when populating array rows. This means they, too, would be
ignored. To get around this, there is a new `addedByServer` property
that flags new fields to ensure they are kept.
This new merge strategy also saves an additional loop over form state.
1. We were merging form state based on a mutable ref. This meant that
changes made within one action cause concurrent actions to have dirty
reads. The fix for this is to merge in an isolated manner by copying
state. This will remove any object references. It is generally not good
practice to mutate state without setting it, anyways, as this causes
mismatches between what is rendered and what is in memory.
1. We were merging server form state directly within an effect, then
replacing state entirely. This meant that if another action took place
at the exact moment in time _after_ merge but _before_ dispatch, the
results of that other action would be completely overridden. The fix for
this is to perform the merge within the reducer itself. This will ensure
that we are working with a trustworthy snapshot of state at the exact
moment in time that the action was invoked, and that React can properly
queue the event within its lifecycle.
|
||
|
|
e87521a376 |
perf(ui): significantly optimize form state component rendering, up to 96% smaller and 75% faster (#11946)
Significantly optimizes the component rendering strategy within the form state endpoint by precisely rendering only the fields that require it. This cuts down on server processing and network response sizes when invoking form state requests **that manipulate array and block rows which contain server components**, such as rich text fields, custom row labels, etc. (results listed below). Here's a breakdown of the issue: Previously, when manipulating array and block fields, _all_ rows would render any server components that might exist within them, including rich text fields. This means that subsequent changes to these fields would potentially _re-render_ those same components even if they don't require it. For example, if you have an array field with a rich text field within it, adding the first row would cause the rich text field to render, which is expected. However, when you add a second row, the rich text field within the first row would render again unnecessarily along with the new row. This is especially noticeable for fields with many rows, where every single row processes its server components and returns RSC data. And this does not only affect nested rich text fields, but any custom component defined on the field level, as these are handled in the same way. The reason this was necessary in the first place was to ensure that the server components receive the proper data when they are rendered, such as the row index and the row's data. Changing one of these rows could cause the server component to receive the wrong data if it was not freshly rendered. While this is still a requirement that rows receive up-to-date props, it is no longer necessary to render everything. Here's a breakdown of the actual fix: This change ensures that only the fields that are actually being manipulated will be rendered, rather than all rows. The existing rows will remain in memory on the client, while the newly rendered components will return from the server. For example, if you add a new row to an array field, only the new row will render its server components. To do this, we send the path of the field that is being manipulated to the server. The server can then use this path to determine for itself which fields have already been rendered and which ones need required rendering. ## Results The following results were gathered by booting up the `form-state` test suite and seeding 100 array rows, each containing a rich text field. To invoke a form state request, we navigate to a document within the "posts" collection, then add a new array row to the list. The result is then saved to the file system for comparison. | Test Suite | Collection | Number of Rows | Before | After | Percentage Change | |------|------|---------|--------|--------|--------| | `form-state` | `posts` | 101 | 1.9MB / 266ms | 80KB / 70ms | ~96% smaller / ~75% faster | --------- Co-authored-by: James <james@trbl.design> Co-authored-by: Alessio Gravili <alessio@gravili.de> |
||
|
|
373f6d1032 |
fix(ui): nested fields disappear when manipulating rows in form state (#11906)
Continuation of #11867. When rendering custom fields nested within arrays or blocks, such as the Lexical rich text editor which is treated as a custom field, these fields will sometimes disappear when form state requests are invoked sequentially. This is especially reproducible on slow networks. This is different from the previous PR in that this issue is caused by adding _rows_ back-to-back, whereas the previous issue was caused when adding a single row followed by a change to another field. Here's a screen recording demonstrating the issue: https://github.com/user-attachments/assets/5ecfa9ec-b747-49ed-8618-df282e64519d The problem is that `requiresRender` is never sent in the form state request for row 2. This is because the [task queue](https://github.com/payloadcms/payload/pull/11579) processes tasks within a single `useEffect`. This forces React to batch the results of these tasks into a single rendering cycle. So if request 1 sets state that request 2 relies on, request 2 will never use that state since they'll execute within the same lifecycle. Here's a play-by-play of the current behavior: 1. The "add row" event is dispatched a. This sets `requiresRender: true` in form state 1. A form state request is sent with `requiresRender: true` 1. While that request is processing, another "add row" event is dispatched a. This sets `requiresRender: true` in form state b. This adds a form state request into the queue 1. The initial form state request finishes a. This sets `requiresRender: false` in form state 1. The next form state request that was queued up in 3b is sent with `requiresRender: false` a. THIS IS EXPECTED, BUT SHOULD ACTUALLY BE `true`!! To fix this this, we need to ensure that the `requiresRender` property is persisted into the second request instead of overridden. To do this, we can add a new `serverPropsToIgnore` to form state which is read when the processing results from the server. So if `requiresRender` exists in `serverPropsToIgnore`, we do not merge it. This works because we actually mutate form state in between requests. So request 2 can read the results from request 1 without going through an additional rendering cycle. Here's a play-by-play of the fix: 1. The "add row" event is dispatched a. This sets `requiresRender: true` in form state b. This adds a task in the queue to mutate form state with `requiresRender: true` 1. A form state request is sent with `requiresRender: true` 1. While that request is processing, another "add row" event is dispatched a. This sets `requiresRender: true` in form state AND `serverPropsToIgnore: [ "requiresRender" ]` c. This adds a form state request into the queue 1. The initial form state request finishes a. This returns `requiresRender: false` from the form state endpoint BUT IS IGNORED 1. The next form state request that was queued up in 3c is sent with `requiresRender: true` |
||
|
|
998181b986 |
feat: query presets (#11330)
Query Presets allow you to save and share filters, columns, and sort orders for your collections. This is useful for reusing common or complex filtering patterns and column configurations across your team. Query Presets are defined on the fly by the users of your app, rather than being hard coded into the Payload Config. Here's a screen recording demonstrating the general workflow as it relates to the list view. Query Presets are not exclusive to the admin panel, however, as they could be useful in a number of other contexts and environments. https://github.com/user-attachments/assets/1fe1155e-ae78-4f59-9138-af352762a1d5 Each Query Preset is saved as a new record in the database under the `payload-query-presets` collection. This will effectively make them CRUDable and allows for an endless number of preset configurations. As you make changes to filters, columns, limit, etc. you can choose to save them as a new record and optionally share them with others. Normal document-level access control will determine who can read, update, and delete these records. Payload provides a set of sensible defaults here, such as "only me", "everyone", and "specific users", but you can also extend your own set of access rules on top of this, such as "by role", etc. Access control is customizable at the operation-level, for example you can set this to "everyone" can read, but "only me" can update. To enable the Query Presets within a particular collection, set `enableQueryPresets` on that collection's config. Here's an example: ```ts { // ... enableQueryPresets: true } ``` Once enabled, a new set of controls will appear within the list view of the admin panel. This is where you can select and manage query presets. General settings for Query Presets are configured under the root `queryPresets` property. This is where you can customize the labels, apply custom access control rules, etc. Here's an example of how you might augment the access control properties with your own custom rule to achieve RBAC: ```ts { // ... queryPresets: { constraints: { read: [ { label: 'Specific Roles', value: 'specificRoles', fields: [roles], access: ({ req: { user } }) => ({ 'access.update.roles': { in: [user?.roles], }, }), }, ], } } } ``` Related: #4193 and #3092 --------- Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com> |
||
|
|
31211e9755 |
feat: pass i18n through field label and description functions (#11802)
Passes the `i18n` arg through field label and description functions.
This is to avoid using custom components when simply needing to
translate a `StaticLabel` object, such as collection labels.
Here's an example:
```ts
{
labels: {
singular: {
en: 'My Collection'
}
},
fields: [
// ...
{
type: 'collapsible',
label: ({ i18n }) => `Translate this: ${getTranslation(collectionConfig.labels.singular, i18n)}`
// ...
}
]
}
```
|
||
|
|
9ea8a7acf0 |
feat: form state select (#11689)
Implements a select-like API into the form state endpoint. This follows the same spec as the Select API on existing Payload operations, but works on form state rather than at the db level. This means you can send the `select` argument through the form state handler, and it will only process and return the fields you've explicitly identified. This is especially useful when you only need to generate a partial form state, for example within the bulk edit form where you select only a subset of fields to edit. There is no need to iterate all fields of the schema, generate default values for each, and return them all through the network. This will also simplify and reduce the amount of client-side processing required, where we longer need to strip unselected fields before submission. |
||
|
|
88eeeaa8dd |
fix: incorrect types for field Label, Description and Error server components (#11642)
Our previous types for Label, Description and Error server components were incorrectly typed. We were using the `ServerProps` type, which was wrong. In our renderFields function, you can see that `ServerComponentProps` are passed as server props, not `ServerProps`: https://github.com/payloadcms/payload/blob/fix/incorrect-component-types/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx Additionally, we no longer have to wrap that type in `Partial<>`, as all server props in that type are required. |
||
|
|
e6fea1d132 |
fix: localized fields within block references were not handled properly if any parent is localized (#11207)
The `localized` properly was not stripped out of referenced block fields, if any parent was localized. For normal fields, this is done in sanitizeConfig. As the same referenced block config can be used in both a localized and non-localized config, we are not able to strip it out inside sanitizeConfig by modifying the block config. Instead, this PR had to bring back tedious logic to handle it everywhere the `field.localized` property is accessed. For backwards-compatibility, we need to keep the existing sanitizeConfig logic. In 4.0, we should remove it to benefit from better test coverage of runtime field.localized handling - for now, this is done for our test suite using the `PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY` flag. |
||
|
|
6eee787493 |
chore: add typescript-strict-plugin to the payload package for incremental file-by-file migration [skip lint] (#11133)
### What? Implement the [typescript-strict-plugin](https://github.com/allegro/typescript-strict-plugin) plugin in the payload (core) package. ### Why? 1. One strategy for incremental migration is to enable strictness rules in tsconfig, fix some errors, and push them without committing the changes to tsconfig.json. However, this is not feasible for a package as large as Payload that has over 1000 typescript errors. Until the work is done, new contributions would undo the work being done. 2. Even if no migration work is done after this PR, this change already improves the strictness of the package. 89 of the 311 files within the package already satisfy strict mode. This PR only adds a comment `@ts-strict-ignore` to files that had at least one compilation error. This way, the propagation of errors in those files is stopped. 3. New files created in the package are strict by default (this was the main improvement in version 2 of `typescript-strict-plugin`). I recommend starting the migration with this package because it is the one that almost all the others depend on. Once we finish this package, we can repeat the same strategy on another one, or use the strategy I mentioned in point 1 if the package is small. ### Note If you don't see errors in the IDE when you uncomment `// @ts-strict-ignore`, try restarting the typescript server or VSCode ### How to contribute to the migration ❤️ 1. Remove `// @ts-strict-ignore` comments from 1 or more files 2. Fix the pending errors (they should appear in your IDE's intellisense or when running `cd packages/payload` + `pnpm build:types` 3. Submit your PR! Important: You don't need to fix everything at once! Furthermore, I recommend breaking this down into very small PRs to trace potential issues later if there are any. So if you have 5 minutes, tackle a small file—every bit counts! 🤗 |
||
|
|
ae32c555ac |
fix(richtext-lexical): ensure sub-fields have access to full document data in form state (#9869)
Fixes https://github.com/payloadcms/payload/issues/10940 This PR does the following: - adds a `useDocumentForm` hook to access the document Form. Useful if you are within a sub-Form - ensure the `data` property passed to field conditions, read access control, validation and filterOptions is always the top-level document data. Previously, for fields within lexical blocks/links/upload, this incorrectly was the lexical block-level data. - adds a `blockData` property to hooks, field conditions, read/update/create field access control, validation and filterOptions for all fields. This allows you to access the data of the nearest parent block, which is especially useful for lexical sub-fields. Users that were previously depending on the incorrect behavior of the `data` property in order to access the data of the lexical block can now switch to the new `blockData` property |
||
|
|
c562fbfa94 |
feat(ui): allows customizing version diff components, render versions ui on the server (#10815)
This PR moves the logic for rendering diff field components in the version comparison view from the client to the server. This allows us to expose more customization options to the server-side Payload Config. For example, users can now pass their own diff components for fields - even including RSCs. This PR also cleans up the version view types Implements the following from https://github.com/payloadcms/payload/discussions/4197: - allow for customization of diff components - more control over versions screens in general TODO: - [x] Bring getFieldPaths fixes into core - [x] Cleanup and test with scrutiny. Ensure all field types display their diffs correctly - [x] Review public API for overriding field types, add docs - [x] Add e2e test for new public API |
||
|
|
82f1bb9864 |
perf: skips field validations until the form is submitted (#10580)
Field validations can be expensive, especially custom validations that are async or highly complex. This can lead to slow form state response times when generating form state for many such fields. Ideally, we only run validations on fields whose values have changed. This is not possible, however, because field validation functions might reference _other_ field values with their args, and there is no good way of detecting exactly which fields should run in this case. The next best thing here is to only run validations _after the form has been submitted_, and then every `onChange` event thereafter until a successful submit has taken place. This is an elegant solution because we currently don't _render_ field errors until submission anyway. This change will significantly speed up form state response times, at least until the form has been submitted. From then on, all field validations will run regardless, just as they do now. If custom validations continue to slow down form state response times, there is a new `event` arg introduced in #10738 that can be used to control whether heavy operations occur on change or on submit. Related: #10638 |
||
|
|
31ae27b67d |
perf: significantly reduce form state response size by up to 3x (#9388)
This significantly optimizes the form state, reducing its size by up to more than 3x and improving overall response times. This change also has rolling effects on initial page size as well, where the initial state for the entire form is sent through the request. To achieve this, we do the following: - Remove `$undefined` strings that are potentially attached to properties like `value`, `initialValue`, `fieldSchema`, etc. - Remove unnecessary properties like empty `errorPaths` arrays and empty `customComponents` objects, which only need to exist if used - Remove unnecessary properties like `valid`, `passesCondition`, etc. which only need to be returned if explicitly `false` - Remove unused properties like `isSidebar`, which simply don't need to exist at all, as they can be easily calculated during render ## Results The following results were gathered by booting up each test suite listed below using the existing seed data, navigating to a document in the relevant collection, then typing a single letter into the noted field in order to invoke new form-state. The result is then saved to the file system for comparison. | Test Suite | Collection | Field | Before | After | Percentage Change | |------|------|---------|--------|--------|--------| | `field-perf` | `blocks-collection` | `layout.0.field1` | 227kB | 110 kB | ~52% smaller | | `fields` | `array-fields` | `items.0.text` | 14 kB | 4 kB | ~72% smaller | | `fields` | `block-fields` | `blocks.0.richText` | 25 kB | 14 kB | ~44% smaller | |
||
|
|
eb037a0cc6 |
fix: passes field permissions to custom fields (#10024)
Fixes #9888. Field permissions were not being passed into custom components. This led to custom components, such as arrays and blocks, unable to render default Payload fields. This was because their props lacked the permissions object required for rendering. For example: ```ts 'use client' import type { ArrayFieldClientComponent } from 'payload' import { ArrayField } from '@payloadcms/ui' export const MyArray: ArrayFieldClientComponent = (props) => <ArrayField {...props} /> ``` In this example the array field itself would render, but the fields within each row would not, because the array field did not pass its permissions down to the rows. |
||
|
|
796df37461 |
fix(ui): awaits form state before rendering conditional fields (#9933)
When a condition exists on a field and it resolves to `false`, it currently "blinks" in and out when rendered within an array or block row. This is because when add rows to form state, we iterate over the _fields_ of that row and render their respective components. Then when conditions are checked for that field, we're expecting `passesCondition` to be explicitly `false`, ultimately _rendering_ the field for a brief moment before form state returns with evaluated conditions. The fix is to set these fields into local form state with a new `isLoading: true` prop, then display a loader within the row until form state returns with its proper conditions. |
||
|
|
fd0ff51296 |
perf: faster page navigation by speeding up createClientConfig, speed up version fetching, speed up lexical init. Up to 100x faster (#9457)
If you had a lot of fields and collections, createClientConfig would be extremely slow, as it was copying a lot of memory. In my test config with a lot of fields and collections, it took 4 seconds(!!). And not only that, it also ran between every single page navigation. This PR significantly speeds up the createClientConfig function. In my test config, its execution speed went from 4 seconds to 50 ms. Additionally, createClientConfig is now properly cached in both dev & prod. It no longer runs between every single page navigation. Even if you trigger a full page reload, createClientConfig will be cached and not run again. Despite that, HMR remains fully-functional. This will make payload feel noticeably faster for large configs - especially if it contains a lot of richtext fields, as it was previously deep-copying the relatively large richText editor configs over and over again. ## Before - 40 sec navigation speed https://github.com/user-attachments/assets/fe6b707a-459b-44c6-982a-b277f6cbb73f ## After - 1 sec navigation speed https://github.com/user-attachments/assets/384fba63-dc32-4396-b3c2-0353fcac6639 ## Todo - [x] Implement ClientSchemaMap and cache it, to remove createClientField call in our form state endpoint - [x] Enable schemaMap caching for dev - [x] Cache lexical clientField generation, or add it to the parent clientConfig ## Lexical changes Red: old / removed Green: new  ### Speed up version queries This PR comes with performance optimizations for fetching versions before a document is loaded. Not only does it use the new select API to limit the fields it queries, it also completely skips a database query if the current document is published. ### Speed up lexical init Removes a bunch of unnecessary deep copying of lexical objects which caused higher memory usage and slower load times. Additionally, the lexical default config sanitization now happens less often. |
||
|
|
7489c29704 |
chore: dedupes field description functions and defers rendering static field descriptions to the client (#9277)
Custom field description functions were being duplicately called in both the Client Config and form state. Static field descriptions were also being rendered in form state unnecessarily. Now, field description functions are only executed once within form state, and static descriptions are deferred to the client for rendering. |
||
|
|
35917c67d7 |
perf(richtext-lexical)!: significantly reduce lexical rerendering and amount of network requests from blocks (#9255)
The field RSC now provides an initial state for all lexical blocks. This completely obliterates any flashes and lexical block loading states when loading or saving a document. Previously, when a document is loaded or saved, every lexical block was sending a network request in order to fetch their form state. Now, this is batched and handled in the lexical server component. All lexical block form states are sent to the client together with the parent lexical field, and are thus available immediately. We also do the same with block collapsed preferences. Thus, there are no loading states or layout shifts/flashes of blocks anymore. Additionally, when saving a document while your cursor is inside a lexical field, the cursor position is preserved. Previously, a document save would kick your cursor out of the lexical field. ## Look at how nice this is: https://github.com/user-attachments/assets/21d736d4-8f80-4df0-a782-7509edd993da **BREAKING:** This removes the `feature.hooks.load` and `feature.hooks.save` interfaces from custom lexical features, as they weren't used internally and added unnecessary, additional overhead. If you have custom features that use those, you can migrate to using normal payload hooks that run on the server instead of the client. |
||
|
|
63cc9668df |
feat(richtext-lexical): allow replacing entire blocks with custom components (#9234)
With this PR, you can now customize the way that `blocks` and `inlineBlocks` are rendered within Lexical's `BlocksFeature` by passing your own React components. This is super helpful when you need to create "previews" or more accurate UI for your Lexical blocks. For example, let's say you have a `gallery` block where your admins select a bunch of images. By default, Lexical would just render a collapsible with your block's fields in it. But now you can customize the `admin.components.Block` property on your `block` config by passing it a custom React component for us to render instead. So using that, with this `gallery` example, you could make a dynamic gallery React component that shows the images to your editors - and then render our built-in `BlockEditButton` to allow your editors to manage your gallery in a drawer. Here is an example where the BlockEditButton is added to the default Block Collapsible/Header:  --------- Co-authored-by: James <james@trbl.design> |
||
|
|
26ffbca914 |
feat: sanitise access endpoint (#7335)
Protects the `/api/access` endpoint behind authentication and sanitizes the result, making it more secure and significantly smaller. To do this: 1. The `permission` keyword is completely omitted from the result 2. Only _truthy_ access results are returned 3. All nested permissions are consolidated when possible --------- Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com> Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com> Co-authored-by: James <james@trbl.design> |
||
|
|
bcbca0e44a |
chore: improves field types (#9172)
### What? Ensures `path` is required and only present on the fields that expect it (all fields except row). Deprecates `useFieldComponents` and `FieldComponentsProvider` and instead extends the RenderField component to account for all field types. This also improves type safety within `RenderField`. ### Why? `path` being optional just adds DX overhead and annoyance. ### How? Added `FieldPaths` type which is added to iterable field types. Placed `path` back onto the ClientFieldBase type. |
||
|
|
f4d526d6e5 |
fix: fallbackLocale not respecting default settings, locale specific fallbacks and not respecting 'none' or false (#8591)
This PR fixes and improves a few things around localisation and fallbackLocale: - For the REST API `fallbackLocale` and `fallback-locale` are treated the same for consistency with the Local API - `fallback: false` in config is now respected, by default results will not fallback to `defaultLocale` unless this config is true, can also be overridden by providing an explicit `fallbackLocale` in the request - locale specific fallbacks will now take priority over `defaultLocale` unless an explicit fallback is provided - Fixes types on operations to allow `'none'` as a value for fallbackLocale - `fallback` is now true by default if unspecified Closes https://github.com/payloadcms/payload/issues/8443 |
||
|
|
c96fa613bc |
feat!: on demand rsc (#8364)
Currently, Payload renders all custom components on initial compile of the admin panel. This is problematic for two key reasons: 1. Custom components do not receive contextual data, i.e. fields do not receive their field data, edit views do not receive their document data, etc. 2. Components are unnecessarily rendered before they are used This was initially required to support React Server Components within the Payload Admin Panel for two key reasons: 1. Fields can be dynamically rendered within arrays, blocks, etc. 2. Documents can be recursively rendered within a "drawer" UI, i.e. relationship fields 3. Payload supports server/client component composition In order to achieve this, components need to be rendered on the server and passed as "slots" to the client. Currently, the pattern for this is to render custom server components in the "client config". Then when a view or field is needed to be rendered, we first check the client config for a "pre-rendered" component, otherwise render our client-side fallback component. But for the reasons listed above, this pattern doesn't exactly make custom server components very useful within the Payload Admin Panel, which is where this PR comes in. Now, instead of pre-rendering all components on initial compile, we're able to render custom components _on demand_, only as they are needed. To achieve this, we've established [this pattern](https://github.com/payloadcms/payload/pull/8481) of React Server Functions in the Payload Admin Panel. With Server Functions, we can iterate the Payload Config and return JSX through React's `text/x-component` content-type. This means we're able to pass contextual props to custom components, such as data for fields and views. ## Breaking Changes 1. Add the following to your root layout file, typically located at `(app)/(payload)/layout.tsx`: ```diff /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ + import type { ServerFunctionClient } from 'payload' import config from '@payload-config' import { RootLayout } from '@payloadcms/next/layouts' import { handleServerFunctions } from '@payloadcms/next/utilities' import React from 'react' import { importMap } from './admin/importMap.js' import './custom.scss' type Args = { children: React.ReactNode } + const serverFunctions: ServerFunctionClient = async function (args) { + 'use server' + return handleServerFunctions({ + ...args, + config, + importMap, + }) + } const Layout = ({ children }: Args) => ( <RootLayout config={config} importMap={importMap} + serverFunctions={serverFunctions} > {children} </RootLayout> ) export default Layout ``` 2. If you were previously posting to the `/api/form-state` endpoint, it no longer exists. Instead, you'll need to invoke the `form-state` Server Function, which can be done through the _new_ `getFormState` utility: ```diff - import { getFormState } from '@payloadcms/ui' - const { state } = await getFormState({ - apiRoute: '', - body: { - // ... - }, - serverURL: '' - }) + const { getFormState } = useServerFunctions() + + const { state } = await getFormState({ + // ... + }) ``` ## Breaking Changes ```diff - useFieldProps() - useCellProps() ``` More details coming soon. --------- Co-authored-by: Alessio Gravili <alessio@gravili.de> Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com> Co-authored-by: James <james@trbl.design> |
||
|
|
03c07026c5 |
feat: add localized indicator to field label (#8602)
On any localized field, appends `locale` on the end of the label. |
||
|
|
87360f23ac |
fix: make field property of FieldLabel optional and partial (#8409)
Fixes https://github.com/payloadcms/payload/issues/8366 |