diff --git a/packages/ui/src/views/Edit/SetDocumentStepNav/index.tsx b/packages/ui/src/views/Edit/SetDocumentStepNav/index.tsx
index 5102947af..181fc2819 100644
--- a/packages/ui/src/views/Edit/SetDocumentStepNav/index.tsx
+++ b/packages/ui/src/views/Edit/SetDocumentStepNav/index.tsx
@@ -19,11 +19,12 @@ export const SetDocumentStepNav: React.FC<{
globalLabel?: SanitizedGlobalConfig['label']
globalSlug?: SanitizedGlobalConfig['slug']
id?: number | string
+ isTrashed?: boolean
pluralLabel?: SanitizedCollectionConfig['labels']['plural']
useAsTitle?: SanitizedCollectionConfig['admin']['useAsTitle']
view?: string
}> = (props) => {
- const { id, collectionSlug, globalSlug, pluralLabel, useAsTitle } = props
+ const { id, collectionSlug, globalSlug, isTrashed, pluralLabel, useAsTitle } = props
const view: string | undefined = props?.view || undefined
@@ -48,6 +49,7 @@ export const SetDocumentStepNav: React.FC<{
if (!isInitializing) {
if (collectionSlug) {
+ // Collection label
nav.push({
label: getTranslation(pluralLabel, i18n),
url: isVisible
@@ -58,13 +60,29 @@ export const SetDocumentStepNav: React.FC<{
: undefined,
})
+ // Trash breadcrumb (if in trash view)
+ if (isTrashed) {
+ nav.push({
+ label: t('general:trash'),
+ url: isVisible
+ ? formatAdminURL({
+ adminRoute,
+ path: `/collections/${collectionSlug}/trash`,
+ })
+ : undefined,
+ })
+ }
+
+ // Document label
if (isEditing) {
nav.push({
label: (useAsTitle && useAsTitle !== 'id' && title) || `${id}`,
url: isVisible
? formatAdminURL({
adminRoute,
- path: `/collections/${collectionSlug}/${id}`,
+ path: isTrashed
+ ? `/collections/${collectionSlug}/trash/${id}`
+ : `/collections/${collectionSlug}/${id}`,
})
: undefined,
})
@@ -85,6 +103,7 @@ export const SetDocumentStepNav: React.FC<{
})
}
+ // Fallback view (used for versions, previews, etc.)
if (view) {
nav.push({
label: view,
@@ -99,6 +118,7 @@ export const SetDocumentStepNav: React.FC<{
isEditing,
pluralLabel,
id,
+ isTrashed,
useAsTitle,
adminRoute,
t,
diff --git a/packages/ui/src/views/Edit/index.tsx b/packages/ui/src/views/Edit/index.tsx
index b4b544686..b621fc87d 100644
--- a/packages/ui/src/views/Edit/index.tsx
+++ b/packages/ui/src/views/Edit/index.tsx
@@ -80,10 +80,12 @@ export function DefaultEditView({
initialState,
isEditing,
isInitializing,
+ isTrashed,
lastUpdateTime,
redirectAfterCreate,
redirectAfterDelete,
redirectAfterDuplicate,
+ redirectAfterRestore,
savedDocumentData,
setCurrentEditor,
setDocumentIsLocked,
@@ -97,6 +99,7 @@ export function DefaultEditView({
drawerSlug,
onDelete,
onDuplicate,
+ onRestore,
onSave: onSaveFromContext,
} = useDocumentDrawerContext()
@@ -471,7 +474,7 @@ export function DefaultEditView({
@@ -546,7 +550,7 @@ export function DefaultEditView({
SaveDraftButton,
}}
data={savedDocumentData}
- disableActions={disableActions || isFolderCollection}
+ disableActions={disableActions || isFolderCollection || isTrashed}
disableCreate={disableCreate}
EditMenuItems={EditMenuItems}
hasPublishPermission={hasPublishPermission}
@@ -554,9 +558,11 @@ export function DefaultEditView({
id={id}
isEditing={isEditing}
isInDrawer={isInDrawer}
+ isTrashed={isTrashed}
onDelete={onDelete}
onDrawerCreateNew={clearDoc}
onDuplicate={onDuplicate}
+ onRestore={onRestore}
onSave={onSave}
onTakeOver={() =>
handleTakeOver(
@@ -576,6 +582,7 @@ export function DefaultEditView({
readOnlyForIncomingUser={isReadOnlyForIncomingUser}
redirectAfterDelete={redirectAfterDelete}
redirectAfterDuplicate={redirectAfterDuplicate}
+ redirectAfterRestore={redirectAfterRestore}
slug={collectionConfig?.slug || globalConfig?.slug}
user={currentEditor}
/>
@@ -637,7 +644,8 @@ export function DefaultEditView({
docPermissions={docPermissions}
fields={docConfig.fields}
forceSidebarWrap={isLivePreviewing}
- readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
+ isTrashed={isTrashed}
+ readOnly={isReadOnlyForIncomingUser || !hasSavePermission || isTrashed}
schemaPathSegments={schemaPathSegments}
/>
{AfterDocument}
diff --git a/packages/ui/src/views/List/ListHeader/index.tsx b/packages/ui/src/views/List/ListHeader/index.tsx
index 49f49afbd..492729058 100644
--- a/packages/ui/src/views/List/ListHeader/index.tsx
+++ b/packages/ui/src/views/List/ListHeader/index.tsx
@@ -1,23 +1,26 @@
import type { I18nClient, TFunction } from '@payloadcms/translations'
-import type { ClientCollectionConfig } from 'payload'
+import type { ClientCollectionConfig, ViewTypes } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import { CloseModalButton } from '../../../elements/CloseModalButton/index.js'
import { useListDrawerContext } from '../../../elements/ListDrawer/Provider.js'
-import { ListFolderPills } from '../../../elements/ListFolderPills/index.js'
import { DrawerRelationshipSelect } from '../../../elements/ListHeader/DrawerRelationshipSelect/index.js'
import { ListDrawerCreateNewDocButton } from '../../../elements/ListHeader/DrawerTitleActions/index.js'
import { ListHeader } from '../../../elements/ListHeader/index.js'
import {
ListBulkUploadButton,
ListCreateNewButton,
+ ListEmptyTrashButton,
} from '../../../elements/ListHeader/TitleActions/index.js'
+import { ByFolderPill } from '../../../elements/ListHeaderTabs/ByFolderPill.js'
+import { DefaultListPill } from '../../../elements/ListHeaderTabs/DefaultListPill.js'
+import './index.scss'
+import { TrashPill } from '../../../elements/ListHeaderTabs/TrashPill.js'
import { useConfig } from '../../../providers/Config/index.js'
import { useListQuery } from '../../../providers/ListQuery/index.js'
import { ListSelection } from '../ListSelection/index.js'
-import './index.scss'
const drawerBaseClass = 'list-drawer'
@@ -29,8 +32,10 @@ export type ListHeaderProps = {
disableBulkDelete?: boolean
disableBulkEdit?: boolean
hasCreatePermission: boolean
+ hasDeletePermission?: boolean
i18n: I18nClient
isBulkUploadEnabled: boolean
+ isTrashEnabled?: boolean
newDocumentURL: string
onBulkUploadSuccess?: () => void
/** @deprecated This prop will be removed in the next major version.
@@ -44,7 +49,7 @@ export type ListHeaderProps = {
/** @deprecated This prop will be removed in the next major version. */
t?: TFunction
TitleActions?: React.ReactNode[]
- viewType?: 'folders' | 'list'
+ viewType?: ViewTypes
}
export const CollectionListHeader: React.FC
= ({
@@ -54,8 +59,10 @@ export const CollectionListHeader: React.FC = ({
disableBulkDelete,
disableBulkEdit,
hasCreatePermission,
+ hasDeletePermission,
i18n,
isBulkUploadEnabled,
+ isTrashEnabled,
newDocumentURL,
onBulkUploadSuccess,
openBulkUpload,
@@ -64,6 +71,7 @@ export const CollectionListHeader: React.FC = ({
}) => {
const { config, getEntityConfig } = useConfig()
const { drawerSlug, isInDrawer, selectedOption } = useListDrawerContext()
+ const isTrashRoute = viewType === 'trash'
const { isGroupingBy } = useListQuery()
if (isInDrawer) {
@@ -108,13 +116,28 @@ export const CollectionListHeader: React.FC = ({
key="list-selection"
label={getTranslation(collectionConfig?.labels?.plural, i18n)}
showSelectAllAcrossPages={!isGroupingBy}
+ viewType={viewType}
+ />
+ ),
+ ((collectionConfig.folders && config.folders) || isTrashEnabled) && (
+
),
collectionConfig.folders && config.folders && (
-
+ ),
+ isTrashEnabled && (
+
),
@@ -123,7 +146,7 @@ export const CollectionListHeader: React.FC = ({
className={className}
title={getTranslation(collectionConfig?.labels?.plural, i18n)}
TitleActions={[
- hasCreatePermission && (
+ hasCreatePermission && !isTrashRoute && (
= ({
newDocumentURL={newDocumentURL}
/>
),
- hasCreatePermission && isBulkUploadEnabled && (
+ hasCreatePermission && isBulkUploadEnabled && !isTrashRoute && (
= ({
openBulkUpload={openBulkUpload}
/>
),
+ hasDeletePermission && isTrashEnabled && viewType === 'trash' && (
+
+ ),
].filter(Boolean)}
/>
)
diff --git a/packages/ui/src/views/List/ListSelection/index.tsx b/packages/ui/src/views/List/ListSelection/index.tsx
index d67dabbe0..d062caf00 100644
--- a/packages/ui/src/views/List/ListSelection/index.tsx
+++ b/packages/ui/src/views/List/ListSelection/index.tsx
@@ -1,5 +1,5 @@
'use client'
-import type { ClientCollectionConfig, Where } from 'payload'
+import type { ClientCollectionConfig, ViewTypes, Where } from 'payload'
import React, { Fragment, useCallback } from 'react'
@@ -7,6 +7,7 @@ import { DeleteMany } from '../../../elements/DeleteMany/index.js'
import { EditMany_v4 } from '../../../elements/EditMany/index.js'
import { ListSelection_v4, ListSelectionButton } from '../../../elements/ListSelection/index.js'
import { PublishMany_v4 } from '../../../elements/PublishMany/index.js'
+import { RestoreMany } from '../../../elements/RestoreMany/index.js'
import { UnpublishMany_v4 } from '../../../elements/UnpublishMany/index.js'
import { SelectAllStatus, useSelection } from '../../../providers/Selection/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
@@ -18,6 +19,7 @@ export type ListSelectionProps = {
label: string
modalPrefix?: string
showSelectAllAcrossPages?: boolean
+ viewType?: ViewTypes
where?: Where
}
@@ -28,6 +30,7 @@ export const ListSelection: React.FC = ({
label,
modalPrefix,
showSelectAllAcrossPages = true,
+ viewType,
where,
}) => {
const { count, selectAll, selectedIDs, toggleAll, totalDocs } = useSelection()
@@ -39,6 +42,8 @@ export const ListSelection: React.FC = ({
return null
}
+ const isTrashView = collectionConfig?.trash && viewType === 'trash'
+
return (
= ({
) : null,
].filter(Boolean)}
SelectionActions={[
- !disableBulkEdit && (
+ !disableBulkEdit && !isTrashView && (
= ({
/>
),
+ isTrashView && (
+
+ ),
!disableBulkDelete && (
-
+
),
].filter(Boolean)}
/>
diff --git a/packages/ui/src/views/List/index.tsx b/packages/ui/src/views/List/index.tsx
index f720bde56..0997d03aa 100644
--- a/packages/ui/src/views/List/index.tsx
+++ b/packages/ui/src/views/List/index.tsx
@@ -4,7 +4,7 @@ import type { ListViewClientProps } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
-import { formatFilesize } from 'payload/shared'
+import { formatAdminURL, formatFilesize } from 'payload/shared'
import React, { Fragment, useEffect } from 'react'
import { useBulkUpload } from '../../elements/BulkUpload/index.js'
@@ -49,6 +49,7 @@ export function DefaultListView(props: ListViewClientProps) {
disableQueryPresets,
enableRowSelections,
hasCreatePermission: hasCreatePermissionFromProps,
+ hasDeletePermission,
listMenuItems,
newDocumentURL,
queryPreset,
@@ -56,6 +57,7 @@ export function DefaultListView(props: ListViewClientProps) {
renderedFilters,
resolvedFilterOptions,
Table: InitialTable,
+ viewType,
} = props
const [Table] = useControllableState(InitialTable)
@@ -69,7 +71,12 @@ export function DefaultListView(props: ListViewClientProps) {
const { user } = useAuth()
- const { getEntityConfig } = useConfig()
+ const {
+ config: {
+ routes: { admin: adminRoute },
+ },
+ getEntityConfig,
+ } = useConfig()
const router = useRouter()
const { data, isGroupingBy } = useListQuery()
@@ -85,6 +92,8 @@ export function DefaultListView(props: ListViewClientProps) {
const isBulkUploadEnabled = isUploadCollection && collectionConfig.upload.bulkUpload
+ const isTrashEnabled = Boolean(collectionConfig.trash)
+
const { i18n } = useTranslation()
const { setStepNav } = useStepNav()
@@ -114,13 +123,27 @@ export function DefaultListView(props: ListViewClientProps) {
useEffect(() => {
if (!isInDrawer) {
- setStepNav([
- {
- label: labels?.plural,
- },
- ])
+ const baseLabel = {
+ label: getTranslation(labels?.plural, i18n),
+ url:
+ isTrashEnabled && viewType === 'trash'
+ ? formatAdminURL({
+ adminRoute,
+ path: `/collections/${collectionSlug}`,
+ })
+ : undefined,
+ }
+
+ const trashLabel = {
+ label: i18n.t('general:trash'),
+ }
+
+ const navItems =
+ isTrashEnabled && viewType === 'trash' ? [baseLabel, trashLabel] : [baseLabel]
+
+ setStepNav(navItems)
}
- }, [setStepNav, labels, isInDrawer])
+ }, [adminRoute, setStepNav, labels, isInDrawer, isTrashEnabled, viewType, i18n, collectionSlug])
return (
@@ -147,12 +170,14 @@ export function DefaultListView(props: ListViewClientProps) {
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
hasCreatePermission={hasCreatePermission}
+ hasDeletePermission={hasDeletePermission}
i18n={i18n}
isBulkUploadEnabled={isBulkUploadEnabled && !upload.hideFileInputOnCreate}
+ isTrashEnabled={isTrashEnabled}
newDocumentURL={newDocumentURL}
openBulkUpload={openBulkUpload}
smallBreak={smallBreak}
- viewType="list"
+ viewType={viewType}
/>
- {i18n.t('general:noResults', { label: getTranslation(labels?.plural, i18n) })}
+ {i18n.t(viewType === 'trash' ? 'general:noTrashResults' : 'general:noResults', {
+ label: getTranslation(labels?.plural, i18n),
+ })}
- {hasCreatePermission && newDocumentURL && (
+ {hasCreatePermission && newDocumentURL && viewType !== 'trash' && (
{isInDrawer ? (