Commit Graph

446 Commits

Author SHA1 Message Date
Paul 8fe5f04831 feat: allow client components to also be used as custom collection views (#16312)
Allows client components to also be used as custom collection views,
bringing it inline with internal views.

A continuation of https://github.com/payloadcms/payload/pull/16243
2026-04-17 19:52:09 +00:00
Paul 835a0ad490 feat(next): add support for custom collection views (#16243)
Originally this PR with extra changes:
https://github.com/payloadcms/payload/pull/15410


- Adds ability to register custom views at the collection level via
`admin.components.views[key]` with a `Component` and `path` — resolves
#15386
- Folders take routing precedence over custom views when both are
defined on an upload collection
- Adds a startup `console.warn` when a custom view is misconfigured
without a `path` property

## Usage

```ts
{
  slug: 'products',
  admin: {
    components: {
      views: {
        grid: {
          Component: '/components/GridView',
          path: '/grid',
          exact: true,
        },
      },
    },
  },
}

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: robinscholz <8195463+robinscholz@users.noreply.github.com>
Co-authored-by: Robin Scholz <robin@maas.engineering>
Co-authored-by: Robin Vey <mail@robinscholz.com>
2026-04-15 19:05:42 +01:00
Patrik 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
2026-02-26 06:47:33 -08:00
German Jablonski 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
2026-02-25 13:33:57 +00:00
Paul 2347cd9e9d feat: move trash out of beta and delete access can now be limited to trash only (#15210)
### 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>
2026-02-19 14:47:41 +00:00
Alessio Gravili 9c8be5cc96 perf(next): avoid re-calculating permissions in some server functions, pass missing args (#15428)
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.
2026-01-30 17:01:11 -05:00
Alessio Gravili ce13e97243 feat(next): pass full initReq context to server functions and dashboard widgets (#15427)
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.
2026-01-30 17:44:13 +00:00
Patrik 94254da4ac feat: add support for custom UnpublishButton component (#15400)
### 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>
2026-01-29 10:01:48 -08:00
Elmo ef863e6b64 feat: add support for custom Status component in document controls (#11154)
### What?

This PR adds the ability to replace the Status section of the document
or global edit view, without the need to replace the whole Edit view.

### Why?

In certain cases there may be a need to add custom elements before or
after the original Status component. In our case, we need to replace the
whole Status component to support custom locale publishing logic and
also for additional status indicators for the users.

### How?

**In the `docs` package:**
- Added a description of the Status component to the components section
within both the collections and globals sections.

**In the `next` package:**
- Enabled rendering of the custom Status component.
- Modified the Live Preview components to include the Status component
prop.

**In the `payload` package:**
- Introduced type definitions for the Status component.
- Implemented importMap generation for the Status component.

**In the `ui` package:**
- Modified the Edit view to pass the Status component to
DocumentControls.
- Updated the DocumentControls component to accept and render a custom
Status component if defined.


Now supports the following Status component in admin

```ts
admin: {
  components: {
    edit: {
      Status: '/components/Status/index.tsx#Status',
    },
  },
},
```

---------

Co-authored-by: Elmo Egers <elmo.egers@smit.ee>
Co-authored-by: Elmo Egers <uplaymedia@gmail.com>
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2026-01-13 19:11:32 +00:00
Jake 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',
            },
          ],
        },
      ],
    },
  ]
}
```
2026-01-09 17:21:17 -05:00
German Jablonski f1116247ed feat: modular dashboards - widgets (#13683)
[RFC Here](https://github.com/payloadcms/payload/discussions/11862).

You can test the feature by running `pnpm dev dashboard` on this branch.

<details>
<summary>Old (obsolete) example</summary>

See the [comment
below](https://github.com/payloadcms/payload/pull/13683#issuecomment-3581457901)
explaining the change in approach we took


https://github.com/user-attachments/assets/96157f83-c5d7-4350-9f31-c014daedb2a8

</details>

### New demo


https://github.com/user-attachments/assets/6c08d8d6-c989-4845-b56f-6d3fbd30b1af

## Future Work 

The following improvements are planned but will be added in the future: 

- fields: You'll be able to define `fields` that a widget receives,
which will serve as props in the component. Why might this be useful?
Imagine a chart widget that can have a weekly, daily, or yearly view. Or
a "count" widget that shows how many documents there are in a collection
(the collection could be a field).
- A11y (EDITED): Okay, I finally went the extra mile here, and you can
reorder and resize it with the keyboard. The screen reader works,
although there's room for improvement.
- Dashboard presets: we're planning to add the ability to create and
share dashboard presets, similar to how [query
presets](https://payloadcms.com/docs/query-presets/overview) work today.
For example, you could build dashboards that adjust based on a variable,
such as a "daily, weekly, or monthly" interval. You could also create
dashboards tailored to different focus areas, like "marketing, sales, or
product."
2025-12-19 15:22:03 +00:00
Jake Fletcher 59a1607a3f feat: support custom slugify functions (#14117)
Continuation of #14007.

Supports overriding the default slugify function of the slug field. This
is necessary if the slug requires special treatment, such as character
encoding, additional language support, etc.

For example, if you wanted to use the
[`slugify`](https://www.npmjs.com/package/slugify) package, you could do
something like this:

```ts
import type { CollectionConfig } from 'payload'
import { slugField } from 'payload'
import slugify from 'slugify';

export const MyCollection: CollectionConfig = {
  // ...
  fields: [
    // ...
    slugField({
      slugify: ({ valueToSlugify }) => slugify(valueToSlugify, {
        // ...additional `slugify` options here
      })
    })
  ]
}
```

This PR also deprecates the old `fieldToUse` arg in favor of `useAsSlug`
which is more semantically clear, following the same convention as
`useAsTitle`.

Example:

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

export const MyCollection: CollectionConfig = {
  // ...
  fields: [
    // ...
    slugField({
      useAsSlug: 'myCustomTitle'
    })
  ]
}
```

In follow-up PRs, we should also:
- Improve the default slugify function to better handle special
characters on its own, etc.
- [Support nested
slugs](https://github.com/payloadcms/payload/pull/14783)
2025-12-02 17:14:48 +00:00
Jarrod Flesch 9f55254da3 fix: relationships should not fallback if fallbackLocale is false (#14641)
Relationship fields would fallback even if the parent of the request set
`fallbackLocale: false`. This was because of the logic in
sanitizeFallbackLocale which would transform `false` value into `null`,
and when a relationship populate function ran, it would transform the
`null` into the `localization.defaultLocale` value.

Added int test that failed before and passes after to cover.
2025-11-17 16:21:51 -05:00
Alessio Gravili 0cc06a1224 fix(ui): relationship field label not updated when document is updated from other drawer (#14609)
When updating a document from a document drawer that isn’t the one
associated with a relationship field, the relationship’s label doesn’t
update correctly.

## Reproduction

This issue is easily reproducible when two relationship fields link to
the same document. If you update the document from the drawer of one
field, the other field will not reflect the update. This happens because
each field only listens to the `onSave` event of its own document
drawer.

## Fix

Use the `useDocumentEvents` hook instead of the `onSave` hook.
`useDocumentEvents` listens for updates to all saved documents, ensuring
that all relationship fields stay in sync regardless of which drawer the
update originated from.

**Before:**


https://github.com/user-attachments/assets/eda152fe-3cda-4111-b0f3-9f0d5a48f37b


**After:**


https://github.com/user-attachments/assets/da252d3d-c0a9-43ee-91d8-508e302d323e
2025-11-17 13:21:24 +00:00
Slava Nossar 339a0c390d fix: use TSiblingData for previousSiblingDoc in FieldHook (#14503)
### What?
In field hooks, `previousSiblingDoc` is typed as `TData`.

### Why?
This value should actually be typed as `TSiblingData`, I've confirmed
the sibling data is actually what is returned when a field hook runs.

### How?
Updated `FieldHook` typing, as well as `RichText` field hook typings

Fixes #9735
2025-11-06 20:59:07 +00:00
Paul 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.
2025-10-23 13:35:22 -04:00
Alessio Gravili 8b0ac01f41 docs: add jsdocs to RichText adapter (#14246)
Adds JSDocs to the RichText adapter return value.
2025-10-18 02:46:58 +03:00
Alessio Gravili 54224c3ab7 perf(richtext-lexical): do not return i18n from editor adapter (#14228)
This PR deprecates the `i18n` property returned from the editor adapter.
Instead, we now merge `i18n` directly into the `config`, since the
config is already available in the richtext adapter provider. This
simplifies the logic (only one line of code) and eliminates the need to
manually read and merge i18n from the adapter.

Benefits:
- **Simpler implementation**: Merging i18n into the config is
straightforward
- **Reduced memory usage**: Each editor no longer maintains its own i18n
object - only a single, merged `config.i18n` is kept.
- **Improved developer experience**: The sanitized field config is now
smaller and easier to inspect. Console logging the config no longer
floods the output with hundreds of translation lines.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211668727538425
2025-10-17 09:19:42 +01:00
Jessica Rynkar 623a1b8216 feat: accept multiple locales in fallbackLocale (#13822)
### What?
This change allows `fallbackLocale` to accept an **array of locales** in
find queries and locale configuration.

```ts
/** Local API **/
  await payload.findByID({
    id,
    collection,
    locale: 'en',
    fallbackLocale: ['fr', 'es'],
  })

/** REST API **/
  await fetch(`${baseURL}/api/${collectionSlug}?locale=en&fallbackLocale[]=fr&fallbackLocale[]=es`)

/** GraphQL **/
  await restClient.GRAPHQL_POST({
    body,
    query: { locale: 'en', fallbackLocale: ['fr', 'es']},
  })

/** Locale Configs **/
  locales: [
    {
      code: 'en',
      label: 'English',
      fallbackLocale: ['fr', 'es'],
    },
  ]
```

### Why?
This update is part of the planned [localization
enhancements](https://github.com/payloadcms/payload/discussions/10705).
Currently, only one fallback locale can be specified. If that locale
doesn’t contain data, the query returns nothing. To work around this,
users have to inspect the response themselves and then make additional
queries for other locales.

With this change, Payload handles that work automatically. Users only
need to provide a list of fallback locales in their preferred order, and
Payload will check each one until it finds a value.

### How?
Updates query handling across the local API, REST API, and GraphQL so
that `fallbackLocale` can accept either a **string** or an **array** of
locales. When an array is passed, Payload will iterate through them in
order and return the first found localized value. This behavior applies
to both collections and globals.

#### Feature request:
https://github.com/payloadcms/payload/discussions/13443

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-10-16 12:02:59 +01:00
Jacob Fletcher 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
2025-10-07 10:15:45 -04:00
Paul d826159fc0 fix(ui): add support back for custom live preview components (#14037)
Fixes https://github.com/payloadcms/payload/issues/13308

Adds support for a custom live preview component back, we previously
supported this and it was allowed via the config types but it wasn't
being rendered.

Now we export the `useLivePreviewContext` hook and the original
`LivePreviewWindow` component too so that end users can wrap the live
preview functionality with anything custom that they may need
2025-10-02 15:09:15 -04:00
Patrik 1d1240fd13 feat: adds admin.formatDocURL function to control list view linking (#13773)
### What?

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

### Why?

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

### How?

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


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

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

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

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

Here's the result:


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

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

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

## Example

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

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


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

# Rationale

## Why not `block.condition`?

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

## Why not call it `blocksFilterOptions`?

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

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211334752795631
2025-09-22 14:20:25 -07:00
Alessio Gravili 1c89291fac feat(richtext-lexical): utility render lexical field on-demand (#13657)
## Why this exists

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

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

## What’s new

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

## Example - Rendering in custom component within existing Form

```tsx
'use client'

import type { JSONFieldClientComponent } from 'payload'

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

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

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

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

```ts
'use client'

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

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

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

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

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

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

## How it works (under the hood)

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

## Type Improvements

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

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

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

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

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

Needs #13790.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211332845301588
2025-09-12 14:52:50 -04:00
Jarrod Flesch 6a0637ecb2 feat(ui): save collection folder tab preferences (#13702)
Saves folder preferences when navigating from list to folder view. This
is a UX improvement so users don't need to click by-folder every time
they go back to the list view.
2025-09-09 11:43:00 -04:00
Alessio Gravili 911f17a887 fix(next): version diff view not handling all field permissions correctly (#13721)
Fixes https://github.com/payloadcms/payload/issues/13286

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

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211267837905375
2025-09-09 11:31:15 -04:00
Jacob Fletcher e49005fcbd feat: unauthenticated client config (#13714)
Secures the client config behind authentication to prevent exposing
underlying schemas to unauthenticated users. Serves an "unauthenticated"
version of the client config in its place, containing only minimally
required properties to enable the login flow.

For example, when downloading the page source for the login view, the
entire client config is embedded in the page response, including all
collection slugs, field names, etc.

This is not inherently insecure. Hooks and core business logic have
already been stripped out, and data is not exposed, but this pattern
could shine light into a project's general structure which could be
considered IP.

This could also be considered a small performance benefit to the login
page, which is now smaller in size relative to the config and can
potentially download faster.

Note: the create first user flow is an acception to the rule. It
requires the full config in order to render fields, including joins,
relationships, etc.

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

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

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

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

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

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

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

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

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

```ts
'use client'

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

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

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

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

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211248751470559
2025-09-08 16:38:00 -04:00
Jacob Fletcher 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
2025-08-22 15:41:51 -04:00
Patrik 4f6d0d8ed2 fix: select field component value prop type does not support array values (#13510)
### What?

Update `SelectFieldBaseClientProps` type so `value` accepts `string[]`
for `hasMany` selects

### Why?

Multi-selects currently error with “Type 'string[]' is not assignable to
type 'string'”.

### How?

- Change `value?: string` to `value?: string | string[]`
- Also adds additional multi select custom component to `admin` test
suite for testing

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-08-19 11:54:52 -07:00
jacobsfletch 0b60bf2eff fix(ui): significantly more predictable autosave form state (#13460) 2025-08-14 19:36:02 -04:00
Jacob Fletcher 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
2025-07-28 23:35:38 -04:00
Patrik 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
2025-07-25 09:08:22 -04:00
Jacob Fletcher bccf6ab16f feat: group by (#13138)
Supports grouping documents by specific fields within the list view.

For example, imagine having a "posts" collection with a "categories"
field. To report on each specific category, you'd traditionally filter
for each category, one at a time. This can be quite inefficient,
especially with large datasets.

Now, you can interact with all categories simultaneously, grouped by
distinct values.

Here is a simple demonstration:


https://github.com/user-attachments/assets/0dcd19d2-e983-47e6-9ea2-cfdd2424d8b5

Enable on any collection by setting the `admin.groupBy` property:

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

const MyCollection: CollectionConfig = {
  // ...
  admin: {
    groupBy: true
  }
}
```

This is currently marked as beta to gather feedback while we reach full
stability, and to leave room for API changes and other modifications.
Use at your own risk.

Note: when using `groupBy`, bulk editing is done group-by-group. In the
future we may support cross-group bulk editing.

Dependent on #13102 (merged).

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210774523852467

---------

Co-authored-by: Paul Popus <paul@payloadcms.com>
2025-07-24 14:00:52 -04:00
Jacob Fletcher d7a3faa4e9 fix(ui): properly sync search params to user preferences (#13200)
Some search params within the list view do not properly sync to user
preferences, and visa versa.

For example, when selecting a query preset, the `?preset=123` param is
injected into the URL and saved to preferences, but when reloading the
page without the param, that preset is not reactivated as expected.

### Problem 

The reason this wasn't working before is that omitting this param would
also reset prefs. It was designed this way in order to support
client-side resets, e.g. clicking the query presets "x" button.

This pattern would never work, however, because this means that every
time the user navigates to the list view directly, their preference is
cleared, as no param would exist in the query.

Note: this is not an issue with _all_ params, as not all are handled in
the same way.

### Solution

The fix is to use empty values instead, e.g. `?preset=`. When the server
receives this, it knows to clear the pref. If it doesn't exist at all,
it knows to load from prefs. And if it has a value, it saves to prefs.
On the client, we sanitize those empty values back out so they don't
appear in the URL in the end.

This PR also refactors much of the list query context and its respective
provider to be significantly more predictable and easier to work with,
namely:

- The `ListQuery` type now fully aligns with what Payload APIs expect,
e.g. `page` is a number, not a string
- The provider now receives a single `query` prop which matches the
underlying context 1:1
- Propagating the query from the server to the URL is significantly more
predictable
- Any new props that may be supported in the future will automatically
work
- No more reconciling `columns` and `listPreferences.columns`, its just
`query.columns`

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210827129744922
2025-07-18 09:29:26 -04:00
Jarrod Flesch 12539c61d4 feat(ui): supports collection scoped folders (#12797)
As discussed in [this
RFC](https://github.com/payloadcms/payload/discussions/12729), this PR
supports collection-scoped folders. You can scope folders to multiple
collection types or just one.

This unlocks the possibility to have folders on a per collection instead
of always being shared on every collection. You can combine this feature
with the `browseByFolder: false` to completely isolate a collection from
other collections.

Things left to do:
- [x] ~~Create a custom react component for the selecting of
collectionSlugs to filter out available options based on the current
folders parameters~~


https://github.com/user-attachments/assets/14cb1f09-8d70-4cb9-b1e2-09da89302995


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210564397815557
2025-07-17 13:24:22 -04:00
Alessio Gravili 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

![Screenshot 2025-06-29 at 12 53
49@2x](https://github.com/user-attachments/assets/962f1513-bc6c-4e12-9b74-9b891c49900b)


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
2025-06-29 14:27:50 -07:00
Jacob Fletcher cfc7adcbc5 fix: strict custom view paths (#12968) 2025-06-29 14:20:54 -04:00
Jacob Fletcher f2213e5c5c feat: mount live preview to document root (#12860)
Mounts live preview to `../:id` instead `../:id/preview`.

This is a huge win for both UX and a maintainability standpoint.

Here are just a few of those wins:

1. If you edit a document, _then_ decide you want to preview those
changes, you are currently presented with the `LeaveWithoutSaving` modal
and are forced to either save your edits or clear them. This is because
you are being navigated to an entirely new page with it's own form
context. Instead, you should be able to freely navigate back and forth
between the two.
2. If you are an editor who most often uses Live Preview, or you are
editing a collection that typically requires it, you likely want it to
automatically enter live preview mode when you open a document.
Currently, the user has to navigate to the document _first_, then use
the live preview tab. Instead, you should be able to set a preference
and avoid this extra step.
3. Since the inception of Live Preview, we've been maintaining largely
the same code across the default edit view and the live preview view,
which often became out of sync and inconsistent—but they're essentially
doing the same thing. While we could abstract a lot of this out, it is
no longer necessary if the two views are combined into one.

This change does also include some small modifications to UI. The "Live
Preview" tab no longer exists, and instead has been replaced with a
button placed next to the document controls (subject to change).

Before:


https://github.com/user-attachments/assets/48518b02-87ba-4750-ba7b-b21b5c75240a

After:


https://github.com/user-attachments/assets/a8ec8657-a6d6-4ee1-b9a7-3c1173bcfa96
2025-06-27 11:58:00 -04:00
Jacob Fletcher c8b72141e4 feat: collection-level preferences (#12909)
Needed for #12860.

The new live preview pattern requires collection-level preferences, a
pattern that does not yet exist.

Instead of creating a new record for these types of preferences, we can
simply reuse `<collectionSlug>-list` under a more general key:
`collection-<slug>`. This way other relevant properties can be attached
in the future that might not specifically apply to the list view.

This will also match the conventions already estalished by
document-level preferences in `collection-<slug>-<id>` and
`global-<slug>`.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210628212784050
2025-06-27 09:08:47 -04:00
Alessio Gravili 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

![CleanShot 2025-04-11 at 20 10
03@2x](https://github.com/user-attachments/assets/a93a500a-3cdd-4cf0-84dd-cf5481aac2b3)

![CleanShot 2025-04-11 at 20 10
28@2x](https://github.com/user-attachments/assets/59bc5885-cbaf-49ea-8d1d-8d145463fd80)

### After

![Screenshot 2025-06-09 at 17 43
49@2x](https://github.com/user-attachments/assets/f6ff0369-76c9-4c1c-9aa7-cbd88806ddc1)

![Screenshot 2025-06-09 at 17 44
50@2x](https://github.com/user-attachments/assets/db93a3db-48d6-4e5d-b080-86a34fff5d22)

![Screenshot 2025-06-09 at 17 45
19@2x](https://github.com/user-attachments/assets/27b6c720-05fe-4957-85af-1305d6b65cfd)

![Screenshot 2025-06-09 at 17 45
34@2x](https://github.com/user-attachments/assets/6d42f458-515a-4611-b27a-f4d6bafbf555)
2025-06-16 07:58:03 -04:00
Kendell Joseph f2e04222f4 feat: admin upload controls (#11615)
### What?
Adds the ability to add additional components to the file upload
component.

```ts
export const Media: CollectionConfig = {
  slug: 'media',
  upload: {
    admin: {
      components: {
        controls: [
          '/collections/components/Control/index.js#UploadControl',
        ],
      },
    },
  },
  fields: [],
}
```

![image](https://github.com/user-attachments/assets/4706e05b-4e95-4f15-8444-a279c589074e)

### Provider
Use the `useUploadControls` provider to either `setUploadControlFile`
passing a file object, or set the file by url using
`setUploadControlFileUrl`.

```tsx
'use client'
import { Button, useUploadControls } from '@payloadcms/ui'
import React, { useCallback } from 'react'

export const UploadControl = () => {
  const { setUploadControlFile, setUploadControlFileUrl } = useUploadControls()

  const loadFromFile = useCallback(async () => {
    const response = await fetch('https://payloadcms.com/images/universal-truth.jpg')
    const blob = await response.blob()
    const file = new File([blob], 'universal-truth.jpg', { type: 'image/jpeg' })
    setUploadControlFile(file)
  }, [setUploadControlFile])

  const loadFromUrl = useCallback(() => {
    setUploadControlFileUrl('https://payloadcms.com/images/universal-truth.jpg')
  }, [setUploadControlFileUrl])

  return (
    <div>
      <Button id="load-from-file-upload-button" onClick={loadFromFile}>
        Load from File
      </Button>
      <br />
      <Button id="load-from-url-upload-button" onClick={loadFromUrl}>
        Load from URL
      </Button>
    </div>
  )
}
```


### Why?
Add the ability to use a custom component to select a document to
upload.
2025-06-13 12:47:46 -04:00
Jessica Rynkar 65309b1d21 feat(next): reorder document view tabs (#12288)
Introduces the ability to customize the order of both default and custom
tabs. This way you can make custom tabs appear before default ones, or
change the order of tabs as you see fit.

To do this, use the new `tab.order` property in your edit view's config:

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

export const MyCollectionConfig: CollectionConfig = {
  // ...
  admin: {
    components: {
      views: {
        edit: {
          myCustomView: {
            path: '/my-custom-view',
            Component: '/path/to/component',
            tab: {
              href: '/my-custom-view',
              order: 100, // This will put this tab in the first position
            },
          }
        }
      }
    }
  }
}
```

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-06-13 15:28:29 +01:00
Jarrod Flesch a43d1a685f feat(ui): moves folder rendering from the client to the server (#12710) 2025-06-10 11:56:28 -04:00
Jarrod Flesch 337f6188da feat: optionally exclude collection documents from appearing in browse-by-folder (#12654)
Adds configurations for browse-by-folder document results. This PR
**does NOT** allow for filtering out folders on a per collection basis.
That will be addressed in a future PR 👍

### Disable browse-by-folder all together
```ts
type RootFoldersConfiguration = {
  /**
   * If true, the browse by folder view will be enabled
   *
   * @default true
   */
  browseByFolder?: boolean
  // ...rest of type
}
```

### Remove document types from appearing in the browse by folder view
```ts
type CollectionFoldersConfiguration =
  | boolean
  | {
      /**
       * If true, the collection documents will be included in the browse by folder view
       *
       * @default true
       */
      browseByFolder?: boolean
    }
```

### Misc
Fixes https://github.com/payloadcms/payload/issues/12631 where adding
folders.collectionOverrides was being set on the client config - it
should be omitted.

Fixes an issue where `baseListFilters` were not being respected.
2025-06-04 13:22:26 -04:00
Elliot DeNolf 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`
2025-06-04 10:15:59 -04:00
Germán Jabloñski 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.
2025-06-03 14:43:37 +00:00
Jessica Rynkar 625d8d9319 feat(ui): adds new editMenuItems custom component (#12649)
## What
Adds a new custom component called `editMenuItems` that can be used in
the document view.
This options allows users to inject their own custom components into the
dropdown menu found in the document controls (the 3 dot menu), the
provided component(s) will be added below the default existing actions
(Create New, Duplicate, Delete and so on).

## Why
To increase flexibility and customization for users who wish to add
functionality to this menu. This provides a clean and consistent way to
add additional actions without needing to override or duplicate existing
UI logic.

## How
- Introduced the `editMenuItems` slot in the document controls dropdown
(three-dot menu) - in edit and preview tabs.
- Added documentation and tests to cover this new custom component

#### Testing
Use the `admin` test suite and go to the `edit menu items` collection
2025-06-03 14:57:48 +01:00