Skip to content

Commit 5e82f9f

Browse files
authored
feat(next): redirect non-existent documents to list view with banner (#13062)
Currently, when a nonexistent document is accessed via the URL, a `NotFound` page is displayed with a button to return to the dashboard. In most cases, the next step the user will take is to navigate to the list of documents in that collection. If we automatically redirect users to the list view and display the error in a banner, we can save them a couple of redirects. This is a very common scenario when writing tests or restarting the local environment. ## Before ![image](https://github.com/user-attachments/assets/ea7af410-5567-4dd2-b44b-67177aa795e6) ## After ![image](https://github.com/user-attachments/assets/72b38d2f-63f2-4a2b-94c4-76ea90d80c24)
1 parent c6105f1 commit 5e82f9f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+140
-15
lines changed

packages/next/src/views/Document/index.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,18 @@ export const renderDocument = async ({
120120
}))
121121

122122
if (isEditing && !doc) {
123-
throw new Error('not-found')
123+
// If it's a collection document that doesn't exist, redirect to collection list
124+
if (collectionSlug) {
125+
const redirectURL = formatAdminURL({
126+
adminRoute,
127+
path: `/collections/${collectionSlug}?notFound=${encodeURIComponent(idFromArgs)}`,
128+
serverURL,
129+
})
130+
redirect(redirectURL)
131+
} else {
132+
// For globals or other cases, keep the 404 behavior
133+
throw new Error('not-found')
134+
}
124135
}
125136

126137
const [

packages/next/src/views/List/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ export const renderListView = async (
225225

226226
const hasCreatePermission = permissions?.collections?.[collectionSlug]?.create
227227

228+
// Check if there's a notFound query parameter (document ID that wasn't found)
229+
const notFoundDocId = typeof searchParams?.notFound === 'string' ? searchParams.notFound : null
230+
228231
const serverProps: ListViewServerPropsOnly = {
229232
collectionConfig,
230233
data,
@@ -248,6 +251,7 @@ export const renderListView = async (
248251
},
249252
collectionConfig,
250253
description: staticDescription,
254+
notFoundDocId,
251255
payload,
252256
serverProps,
253257
})

packages/next/src/views/List/renderListViewSlots.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ import type {
1616
ViewDescriptionServerPropsOnly,
1717
} from 'payload'
1818

19+
import { Banner } from '@payloadcms/ui/elements/Banner'
1920
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
21+
import React from 'react'
2022

2123
type Args = {
2224
clientProps: ListViewSlotSharedClientProps
2325
collectionConfig: SanitizedCollectionConfig
2426
description?: StaticDescription
27+
notFoundDocId?: null | string
2528
payload: Payload
2629
serverProps: ListViewServerPropsOnly
2730
}
@@ -30,6 +33,7 @@ export const renderListViewSlots = ({
3033
clientProps,
3134
collectionConfig,
3235
description,
36+
notFoundDocId,
3337
payload,
3438
serverProps,
3539
}: Args): ListViewSlots => {
@@ -75,13 +79,31 @@ export const renderListViewSlots = ({
7579
})
7680
}
7781

78-
if (collectionConfig.admin.components?.beforeListTable) {
79-
result.BeforeListTable = RenderServerComponent({
80-
clientProps: clientProps satisfies BeforeListTableClientProps,
81-
Component: collectionConfig.admin.components.beforeListTable,
82-
importMap: payload.importMap,
83-
serverProps: serverProps satisfies BeforeListTableServerPropsOnly,
84-
})
82+
// Handle beforeListTable with optional banner
83+
const existingBeforeListTable = collectionConfig.admin.components?.beforeListTable
84+
? RenderServerComponent({
85+
clientProps: clientProps satisfies BeforeListTableClientProps,
86+
Component: collectionConfig.admin.components.beforeListTable,
87+
importMap: payload.importMap,
88+
serverProps: serverProps satisfies BeforeListTableServerPropsOnly,
89+
})
90+
: null
91+
92+
// Create banner for document not found
93+
const notFoundBanner = notFoundDocId ? (
94+
<Banner type="error">
95+
{serverProps.i18n.t('error:documentNotFound', { id: notFoundDocId })}
96+
</Banner>
97+
) : null
98+
99+
// Combine banner and existing component
100+
if (notFoundBanner || existingBeforeListTable) {
101+
result.BeforeListTable = (
102+
<React.Fragment>
103+
{notFoundBanner}
104+
{existingBeforeListTable}
105+
</React.Fragment>
106+
)
85107
}
86108

87109
if (collectionConfig.admin.components?.Description) {

packages/translations/src/clientKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
6565
'error:autosaving',
6666
'error:correctInvalidFields',
6767
'error:deletingTitle',
68+
'error:documentNotFound',
6869
'error:emailOrPasswordIncorrect',
6970
'error:usernameOrPasswordIncorrect',
7071
'error:loadingDocument',

packages/translations/src/languages/ar.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export const arTranslations: DefaultTranslationsObject = {
8686
deletingFile: 'حدث خطأ أثناء حذف الملف.',
8787
deletingTitle:
8888
'حدث خطأ أثناء حذف {{title}}. يرجى التحقق من الاتصال الخاص بك والمحاولة مرة أخرى.',
89+
documentNotFound:
90+
'لم يتم العثور على المستند بالمعرف {{id}}. قد يكون قد تم حذفه أو لم يكن موجودًا أصلاً ، أو قد لا يكون لديك الوصول إليه.',
8991
emailOrPasswordIncorrect: 'البريد الإلكتروني أو كلمة المرور المقدمة غير صحيحة.',
9092
followingFieldsInvalid_one: 'الحقل التالي غير صالح:',
9193
followingFieldsInvalid_other: 'الحقول التالية غير صالحة:',

packages/translations/src/languages/az.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export const azTranslations: DefaultTranslationsObject = {
8686
deletingFile: 'Faylın silinməsində xəta baş verdi.',
8787
deletingTitle:
8888
'{{title}} silinərkən xəta baş verdi. Zəhmət olmasa, bağlantınızı yoxlayın və yenidən cəhd edin.',
89+
documentNotFound:
90+
'{{id}} ID-li sənəd tapılmadı. Bu, onun silinmiş və ya heç vaxt mövcud olmamış ola bilər və ya sizin ona giriş hüququnuz olmayabilir.',
8991
emailOrPasswordIncorrect: 'Təqdim olunan e-poçt və ya şifrə yanlışdır.',
9092
followingFieldsInvalid_one: 'Aşağıdakı sahə yanlışdır:',
9193
followingFieldsInvalid_other: 'Aşağıdaki sahələr yanlışdır:',

packages/translations/src/languages/bg.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export const bgTranslations: DefaultTranslationsObject = {
8686
deletingFile: 'Имаше грешка при изтриването на файла.',
8787
deletingTitle:
8888
'Имаше проблем при изтриването на {{title}}. Моля провери връзката си и опитай отново.',
89+
documentNotFound:
90+
'Документът с ID {{id}} не можа да бъде намерен. Възможно е да е бил изтрит или никога да не е съществувал или може би нямате достъп до него.',
8991
emailOrPasswordIncorrect: 'Имейлът или паролата не са правилни.',
9092
followingFieldsInvalid_one: 'Следното поле е некоректно:',
9193
followingFieldsInvalid_other: 'Следните полета са некоректни:',

packages/translations/src/languages/bnBd.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export const bnBdTranslations: DefaultTranslationsObject = {
8686
deletingFile: 'ফাইল মুছতে একটি ত্রুটি হয়েছে।',
8787
deletingTitle:
8888
'{{title}} মুছতে একটি ত্রুটি হয়েছে। আপনার সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।',
89+
documentNotFound:
90+
'আইডি {{id}} এর সাথে সম্পর্কিত ডকুমেন্টটি পাওয়া যাচ্ছে না। এটি মোছা হয়েছে বা কখনই না থাকতে পারে, অথবা আপনার এর প্রবেশাধিকার না থ',
8991
emailOrPasswordIncorrect: 'প্রদত্ত ইমেইল বা পাসওয়ার্ড ভুল।',
9092
followingFieldsInvalid_one: 'নিম্নলিখিত ক্ষেত্রটি অবৈধ:',
9193
followingFieldsInvalid_other: 'নিম্নলিখিত ক্ষেত্রগুলি অবৈধ:',

packages/translations/src/languages/bnIn.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export const bnInTranslations: DefaultTranslationsObject = {
8686
deletingFile: 'ফাইল মুছতে একটি ত্রুটি হয়েছে।',
8787
deletingTitle:
8888
'{{title}} মুছতে একটি ত্রুটি হয়েছে। আপনার সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।',
89+
documentNotFound:
90+
'ID সহ {{id}} ডকুমেন্টটি পাওয়া যায়নি। এটি মুছে ফেলা হতে পারে বা কখনই ছিল না, অথবা আপনার এটির অ্যাক্সেস নেই।',
8991
emailOrPasswordIncorrect: 'প্রদত্ত ইমেইল বা পাসওয়ার্ড ভুল।',
9092
followingFieldsInvalid_one: 'নিম্নলিখিত ক্ষেত্রটি অবৈধ:',
9193
followingFieldsInvalid_other: 'নিম্নলিখিত ক্ষেত্রগুলি অবৈধ:',

packages/translations/src/languages/ca.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export const caTranslations: DefaultTranslationsObject = {
8686
deletingFile: "Hi ha hagut un error en eliminar l'arxiu.",
8787
deletingTitle:
8888
"Hi ha hagut un error mentre s'eliminava {{title}}. Si us plau, comprova la teva connexió i torna-ho a intentar.",
89+
documentNotFound:
90+
"El document amb ID {{id}} no s'ha pogut trobar. Pot haver estat esborrat o mai haver existit, o potser no tens accés a aquest.",
8991
emailOrPasswordIncorrect:
9092
'El correu electrònic o la contrasenya proporcionats no són correctes.',
9193
followingFieldsInvalid_one: 'El següent camp no és vàlid:',

0 commit comments

Comments
 (0)