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
This commit is contained in:
@@ -60,32 +60,33 @@ export const Posts: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. [More details](../database/indexes#compound-indexes). |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
|
||||
| Option | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| `trash` | A boolean to enable soft deletes for this collection. Defaults to `false`. [More details](../trash/overview). |
|
||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export default async function Page() {
|
||||
collection: 'pages',
|
||||
id: '123',
|
||||
draft: true,
|
||||
trash: true, // add this if trash is enabled in your collection and want to preview trashed documents
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
200
docs/trash/overview.mdx
Normal file
200
docs/trash/overview.mdx
Normal file
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: Trash
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Enable soft deletes for your collections to mark documents as deleted without permanently removing them.
|
||||
keywords: trash, soft delete, deletedAt, recovery, restore
|
||||
---
|
||||
|
||||
Trash (also known as soft delete) allows documents to be marked as deleted without being permanently removed. When enabled on a collection, deleted documents will receive a `deletedAt` timestamp, making it possible to restore them later, view them in a dedicated Trash view, or permanently delete them.
|
||||
|
||||
Soft delete is a safer way to manage content lifecycle, giving editors a chance to review and recover documents that may have been deleted by mistake.
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** The Trash feature is currently in beta and may be subject to change
|
||||
in minor version updates.
|
||||
</Banner>
|
||||
|
||||
## Collection Configuration
|
||||
|
||||
To enable soft deleting for a collection, set the `trash` property to `true`:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
trash: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
// other fields...
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
When enabled, Payload automatically injects a deletedAt field into the collection's schema. This timestamp is set when a document is soft-deleted, and cleared when the document is restored.
|
||||
|
||||
## Admin Panel behavior
|
||||
|
||||
Once `trash` is enabled, the Admin Panel provides a dedicated Trash view for each collection:
|
||||
|
||||
- A new route is added at `/collections/:collectionSlug/trash`
|
||||
- The `Trash` view shows all documents that have a `deletedAt` timestamp
|
||||
|
||||
From the Trash view, you can:
|
||||
|
||||
- Use bulk actions to manage trashed documents:
|
||||
|
||||
- **Restore** to clear the `deletedAt` timestamp and return documents to their original state
|
||||
- **Delete** to permanently remove selected documents
|
||||
- **Empty Trash** to select and permanently delete all trashed documents at once
|
||||
|
||||
- Enter each document's **edit view**, just like in the main list view. While in the edit view of a trashed document:
|
||||
- All fields are in a **read-only** state
|
||||
- Standard document actions (e.g., Save, Publish, Restore Version) are hidden and disabled.
|
||||
- The available actions are **Restore** and **Permanently Delete**.
|
||||
- Access to the **API**, **Versions**, and **Preview** views is preserved.
|
||||
|
||||
When deleting a document from the main collection List View, Payload will soft-delete the document by default. A checkbox in the delete confirmation modal allows users to skip the trash and permanently delete instead.
|
||||
|
||||
## API Support
|
||||
|
||||
Soft deletes are fully supported across all Payload APIs: **Local**, **REST**, and **GraphQL**.
|
||||
|
||||
The following operations respect and support the `trash` functionality:
|
||||
|
||||
- `find`
|
||||
- `findByID`
|
||||
- `update`
|
||||
- `updateByID`
|
||||
- `delete`
|
||||
- `deleteByID`
|
||||
- `findVersions`
|
||||
- `findVersionByID`
|
||||
|
||||
### Understanding `trash` Behavior
|
||||
|
||||
Passing `trash: true` to these operations will **include soft-deleted documents** in the query results.
|
||||
|
||||
To return _only_ soft-deleted documents, you must combine `trash: true` with a `where` clause that checks if `deletedAt` exists.
|
||||
|
||||
### Examples
|
||||
|
||||
#### Local API
|
||||
|
||||
Return all documents including trashed:
|
||||
|
||||
```ts
|
||||
const result = await payload.find({
|
||||
collection: 'posts',
|
||||
trash: true,
|
||||
})
|
||||
```
|
||||
|
||||
Return only trashed documents:
|
||||
|
||||
```ts
|
||||
const result = await payload.find({
|
||||
collection: 'posts',
|
||||
trash: true,
|
||||
where: {
|
||||
deletedAt: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Return only non-trashed documents:
|
||||
|
||||
```ts
|
||||
const result = await payload.find({
|
||||
collection: 'posts',
|
||||
trash: false,
|
||||
})
|
||||
```
|
||||
|
||||
#### REST
|
||||
|
||||
Return **all** documents including trashed:
|
||||
|
||||
```http
|
||||
GET /api/posts?trash=true
|
||||
```
|
||||
|
||||
Return **only trashed** documents:
|
||||
|
||||
```http
|
||||
GET /api/posts?trash=true&where[deletedAt][exists]=true
|
||||
```
|
||||
|
||||
Return only non-trashed documents:
|
||||
|
||||
```http
|
||||
GET /api/posts?trash=false
|
||||
```
|
||||
|
||||
#### GraphQL
|
||||
|
||||
Return all documents including trashed:
|
||||
|
||||
```ts
|
||||
query {
|
||||
Posts(trash: true) {
|
||||
docs {
|
||||
id
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Return only trashed documents:
|
||||
|
||||
```ts
|
||||
query {
|
||||
Posts(
|
||||
trash: true
|
||||
where: { deletedAt: { exists: true } }
|
||||
) {
|
||||
docs {
|
||||
id
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Return only non-trashed documents:
|
||||
|
||||
```ts
|
||||
query {
|
||||
Posts(trash: false) {
|
||||
docs {
|
||||
id
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Access Control
|
||||
|
||||
All trash-related actions (delete, permanent delete) respect the `delete` access control defined in your collection config.
|
||||
|
||||
This means:
|
||||
|
||||
- If a user is denied delete access, they cannot soft delete or permanently delete documents
|
||||
|
||||
## Versions and Trash
|
||||
|
||||
When a document is soft-deleted:
|
||||
|
||||
- It can no longer have a version **restored** until it is first restored from trash
|
||||
- Attempting to restore a version while the document is in trash will result in an error
|
||||
- This ensures consistency between the current document state and its version history
|
||||
|
||||
However, versions are still fully **visible and accessible** from the **edit view** of a trashed document. You can view the full version history, but must restore the document itself before restoring any individual version.
|
||||
Reference in New Issue
Block a user