diff --git a/docs/admin/components.mdx b/docs/admin/components.mdx
index ac6bb81048..7ebd82f3c7 100644
--- a/docs/admin/components.mdx
+++ b/docs/admin/components.mdx
@@ -96,7 +96,7 @@ To swap out any of these views, simply pass in your custom component to the `adm
}
```
-For more granular control, pass a configuration object instead. Payload exposes all of the properties of `` component in [React Router v5](https://v5.reactrouter.com):
+For more granular control, pass a configuration object instead. Each view corresponds to its own `` component in [React Router v5](https://v5.reactrouter.com). Payload exposes all of the properties of React Router:
| Property | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
@@ -129,6 +129,12 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob
}
```
+
+ Note:
+
+ Routes are cascading. This means that unless explicitly given the `exact` property, they will match on URLs that simply _start_ with the route's path. This is helpful when creating catch-all routes in your application. Alternatively, you could define your nested route _before_ your parent route.
+
+
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._
For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).
diff --git a/test/admin/collections/CustomViews2.ts b/test/admin/collections/CustomViews2.ts
index 3656ea5a75..d39b880459 100644
--- a/test/admin/collections/CustomViews2.ts
+++ b/test/admin/collections/CustomViews2.ts
@@ -1,9 +1,17 @@
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
import CustomTabComponent from '../components/CustomTabComponent'
+import CustomTabView from '../components/views/CustomTab'
+import CustomTabView2 from '../components/views/CustomTab2'
+import CustomNestedTabView from '../components/views/CustomTabNested'
import CustomVersionsView from '../components/views/CustomVersions'
-import CustomView from '../components/views/CustomView'
-import { customEditLabel, customTabLabel, customViews2Slug } from '../shared'
+import {
+ customEditLabel,
+ customNestedTabViewPath,
+ customTabLabel,
+ customTabViewPath,
+ customViews2Slug,
+} from '../shared'
export const CustomViews2: CollectionConfig = {
slug: customViews2Slug,
@@ -21,17 +29,21 @@ export const CustomViews2: CollectionConfig = {
Versions: CustomVersionsView,
MyCustomView: {
path: '/custom-tab-view',
- Component: CustomView,
+ Component: CustomTabView,
Tab: {
label: customTabLabel,
href: '/custom-tab-view',
},
},
MyCustomViewWithCustomTab: {
- path: '/custom-tab-component',
- Component: CustomView,
+ path: customTabViewPath,
+ Component: CustomTabView2,
Tab: CustomTabComponent,
},
+ MyCustomViewWithNestedPath: {
+ path: customNestedTabViewPath,
+ Component: CustomNestedTabView,
+ },
},
},
},
diff --git a/test/admin/components/views/CustomDefaultEdit/index.tsx b/test/admin/components/views/CustomEditDefault/index.tsx
similarity index 100%
rename from test/admin/components/views/CustomDefaultEdit/index.tsx
rename to test/admin/components/views/CustomEditDefault/index.tsx
diff --git a/test/admin/components/views/CustomTab/index.tsx b/test/admin/components/views/CustomTab/index.tsx
new file mode 100644
index 0000000000..aa3301ce02
--- /dev/null
+++ b/test/admin/components/views/CustomTab/index.tsx
@@ -0,0 +1,42 @@
+import React, { Fragment, useEffect } from 'react'
+
+import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
+import { type AdminViewComponent } from '../../../../../packages/payload/src/config/types'
+import { customTabViewTitle } from '../../../shared'
+
+const CustomTabView: AdminViewComponent = () => {
+ const { setStepNav } = useStepNav()
+
+ // This effect will only run one time and will allow us
+ // to set the step nav to display our custom route name
+
+ useEffect(() => {
+ setStepNav([
+ {
+ label: 'Custom Tab View',
+ },
+ ])
+ }, [setStepNav])
+
+ return (
+
+
+
{customTabViewTitle}
+
This custom view was added through the Payload config:
+
+ -
+
components.views[key].Component
+
+
+
+
+ )
+}
+
+export default CustomTabView
diff --git a/test/admin/components/views/CustomTab2/index.tsx b/test/admin/components/views/CustomTab2/index.tsx
new file mode 100644
index 0000000000..1778b3b2b9
--- /dev/null
+++ b/test/admin/components/views/CustomTab2/index.tsx
@@ -0,0 +1,42 @@
+import React, { Fragment, useEffect } from 'react'
+
+import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
+import { type AdminViewComponent } from '../../../../../packages/payload/src/config/types'
+import { customTabViewTitle } from '../../../shared'
+
+const CustomTabView2: AdminViewComponent = () => {
+ const { setStepNav } = useStepNav()
+
+ // This effect will only run one time and will allow us
+ // to set the step nav to display our custom route name
+
+ useEffect(() => {
+ setStepNav([
+ {
+ label: 'Custom Tab View',
+ },
+ ])
+ }, [setStepNav])
+
+ return (
+
+
+
{customTabViewTitle}
+
This custom view was added through the Payload config:
+
+ -
+
components.views[key].Component
+
+
+
+
+ )
+}
+
+export default CustomTabView2
diff --git a/test/admin/components/views/CustomTabNested/index.tsx b/test/admin/components/views/CustomTabNested/index.tsx
new file mode 100644
index 0000000000..9dd561078f
--- /dev/null
+++ b/test/admin/components/views/CustomTabNested/index.tsx
@@ -0,0 +1,42 @@
+import React, { Fragment, useEffect } from 'react'
+
+import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
+import { type AdminViewComponent } from '../../../../../packages/payload/src/config/types'
+import { customNestedTabViewTitle } from '../../../shared'
+
+const CustomNestedTabView: AdminViewComponent = () => {
+ const { setStepNav } = useStepNav()
+
+ // This effect will only run one time and will allow us
+ // to set the step nav to display our custom route name
+
+ useEffect(() => {
+ setStepNav([
+ {
+ label: 'Custom Nested View',
+ },
+ ])
+ }, [setStepNav])
+
+ return (
+
+
+
{customNestedTabViewTitle}
+
This custom view was added through the Payload config:
+
+ -
+
components.views[key].Component
+
+
+
+
+ )
+}
+
+export default CustomNestedTabView
diff --git a/test/admin/components/views/CustomView/index.tsx b/test/admin/components/views/CustomView/index.tsx
index 14236d5ae0..c2cf4539e7 100644
--- a/test/admin/components/views/CustomView/index.tsx
+++ b/test/admin/components/views/CustomView/index.tsx
@@ -1,40 +1,25 @@
-import React, { Fragment, useEffect } from 'react'
+import React from 'react'
-import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
import { type AdminViewComponent } from '../../../../../packages/payload/src/config/types'
+import { customViewTitle } from '../../../shared'
const CustomView: AdminViewComponent = () => {
- const { setStepNav } = useStepNav()
-
- // This effect will only run one time and will allow us
- // to set the step nav to display our custom route name
-
- useEffect(() => {
- setStepNav([
- {
- label: 'Custom View',
- },
- ])
- }, [setStepNav])
-
return (
-
-
-
Custom View
-
This custom view was added through the Payload config:
-
- -
-
components.views[key].Component
-
-
-
-
+
+
{customViewTitle}
+
This custom view was added through the Payload config:
+
+ -
+
components.views[key].Component
+
+
+
)
}
diff --git a/test/admin/components/views/CustomViewNested/index.tsx b/test/admin/components/views/CustomViewNested/index.tsx
new file mode 100644
index 0000000000..70f506a5f1
--- /dev/null
+++ b/test/admin/components/views/CustomViewNested/index.tsx
@@ -0,0 +1,26 @@
+import React from 'react'
+
+import { type AdminViewComponent } from '../../../../../packages/payload/src/config/types'
+import { customNestedViewTitle } from '../../../shared'
+
+const CustomNestedView: AdminViewComponent = () => {
+ return (
+
+
{customNestedViewTitle}
+
This custom view was added through the Payload config:
+
+ -
+
components.views[key].Component
+
+
+
+ )
+}
+
+export default CustomNestedView
diff --git a/test/admin/config.ts b/test/admin/config.ts
index b9a2ce5436..4536e156f7 100644
--- a/test/admin/config.ts
+++ b/test/admin/config.ts
@@ -1,9 +1,7 @@
import path from 'path'
-import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
-import { devUser } from '../credentials'
-import { CustomViews1, customViews1Slug } from './collections/CustomViews1'
+import { CustomViews1 } from './collections/CustomViews1'
import { CustomViews2 } from './collections/CustomViews2'
import { Geo } from './collections/Geo'
import { CollectionGroup1A } from './collections/Group1A'
@@ -20,6 +18,8 @@ import BeforeLogin from './components/BeforeLogin'
import Logout from './components/Logout'
import CustomDefaultView from './components/views/CustomDefault'
import CustomMinimalRoute from './components/views/CustomMinimal'
+import CustomView from './components/views/CustomView'
+import CustomNestedView from './components/views/CustomViewNested'
import { CustomGlobalViews1 } from './globals/CustomViews1'
import { CustomGlobalViews2 } from './globals/CustomViews2'
import { Global } from './globals/Global'
@@ -27,7 +27,8 @@ import { GlobalGroup1A } from './globals/Group1A'
import { GlobalGroup1B } from './globals/Group1B'
import { GlobalHidden } from './globals/Hidden'
import { GlobalNoApiView } from './globals/NoApiView'
-import { customViews2Slug, noApiViewCollection, postsSlug } from './shared'
+import { seed } from './seed'
+import { customNestedViewPath, customViewPath } from './shared'
export interface Post {
createdAt: Date
@@ -51,14 +52,23 @@ export default buildConfigWithDefaults({
views: {
// Dashboard: CustomDashboardView,
// Account: CustomAccountView,
- CustomMinimalRoute: {
+ CustomMinimalView: {
path: '/custom-minimal-view',
Component: CustomMinimalRoute,
},
- CustomDefaultRoute: {
+ CustomDefaultView: {
path: '/custom-default-view',
Component: CustomDefaultView,
},
+ CustomView: {
+ path: customViewPath,
+ exact: true,
+ Component: CustomView,
+ },
+ CustomNestedView: {
+ path: customNestedViewPath,
+ Component: CustomNestedView,
+ },
},
},
},
@@ -97,56 +107,5 @@ export default buildConfigWithDefaults({
GlobalGroup1A,
GlobalGroup1B,
],
- onInit: async (payload) => {
- await payload.create({
- collection: 'users',
- data: {
- email: devUser.email,
- password: devUser.password,
- },
- })
-
- await mapAsync([...Array(11)], async () => {
- await payload.create({
- collection: postsSlug,
- data: {
- title: 'Title',
- description: 'Description',
- },
- })
- })
-
- await payload.create({
- collection: customViews1Slug,
- data: {
- title: 'Custom View',
- },
- })
-
- await payload.create({
- collection: customViews2Slug,
- data: {
- title: 'Custom View',
- },
- })
-
- await payload.create({
- collection: 'geo',
- data: {
- point: [7, -7],
- },
- })
-
- await payload.create({
- collection: 'geo',
- data: {
- point: [5, -5],
- },
- })
-
- await payload.create({
- collection: noApiViewCollection,
- data: {},
- })
- },
+ onInit: seed,
})
diff --git a/test/admin/e2e.spec.ts b/test/admin/e2e.spec.ts
index 62830cb9b7..efec62c310 100644
--- a/test/admin/e2e.spec.ts
+++ b/test/admin/e2e.spec.ts
@@ -22,7 +22,15 @@ import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers'
import {
customEditLabel,
+ customNestedTabViewPath,
+ customNestedTabViewTitle,
+ customNestedViewPath,
+ customNestedViewTitle,
customTabLabel,
+ customTabViewPath,
+ customTabViewTitle,
+ customViewPath,
+ customViewTitle,
customViews2Slug,
globalSlug,
group1Collection1Slug,
@@ -164,6 +172,44 @@ describe('admin', () => {
await expect(page.locator('.not-found')).toContainText('Nothing found')
})
+ test('should render custom view', async () => {
+ await page.goto(`${serverURL}/admin${customViewPath}`)
+ const pageURL = page.url()
+ const pathname = new URL(pageURL).pathname
+ expect(pathname).toEqual(`/admin${customViewPath}`)
+ await expect(page.locator('h1#custom-view-title')).toContainText(customViewTitle)
+ })
+
+ test('should render custom nested view', async () => {
+ await page.goto(`${serverURL}/admin${customNestedViewPath}`)
+ const pageURL = page.url()
+ const pathname = new URL(pageURL).pathname
+ expect(pathname).toEqual(`/admin${customNestedViewPath}`)
+ await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
+ })
+
+ test('collection - should render custom tab view', async () => {
+ await page.goto(customViewsURL.create)
+ await page.locator('#field-title').fill('Test')
+ await saveDocAndAssert(page)
+ const pageURL = page.url()
+ const customViewURL = `${pageURL}${customTabViewPath}`
+ await page.goto(customViewURL)
+ expect(page.url()).toEqual(customViewURL)
+ await expect(page.locator('h1#custom-view-title')).toContainText(customTabViewTitle)
+ })
+
+ test('collection - should render custom nested tab view', async () => {
+ await page.goto(customViewsURL.create)
+ await page.locator('#field-title').fill('Test')
+ await saveDocAndAssert(page)
+ const pageURL = page.url()
+ const customNestedTabViewURL = `${pageURL}${customNestedTabViewPath}`
+ await page.goto(customNestedTabViewURL)
+ expect(page.url()).toEqual(customNestedTabViewURL)
+ await expect(page.locator('h1#custom-view-title')).toContainText(customNestedTabViewTitle)
+ })
+
test('collection - should render custom tab label', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
diff --git a/test/admin/globals/CustomViews2.ts b/test/admin/globals/CustomViews2.ts
index dcb95d2642..6c3339f4fd 100644
--- a/test/admin/globals/CustomViews2.ts
+++ b/test/admin/globals/CustomViews2.ts
@@ -1,9 +1,9 @@
import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types'
import CustomTabComponent from '../components/CustomTabComponent'
-import CustomDefaultEditView from '../components/views/CustomDefaultEdit'
+import CustomDefaultEditView from '../components/views/CustomEditDefault'
+import CustomView from '../components/views/CustomTab'
import CustomVersionsView from '../components/views/CustomVersions'
-import CustomView from '../components/views/CustomView'
export const CustomGlobalViews2: GlobalConfig = {
slug: 'custom-global-views-two',
diff --git a/test/admin/seed/index.ts b/test/admin/seed/index.ts
new file mode 100644
index 0000000000..e2cedfa63d
--- /dev/null
+++ b/test/admin/seed/index.ts
@@ -0,0 +1,59 @@
+import type { Config } from '../../../packages/payload/src/config/types'
+
+import { mapAsync } from '../../../packages/payload/src/utilities/mapAsync'
+import { devUser } from '../../credentials'
+import { customViews1Slug } from '../collections/CustomViews1'
+import { customViews2Slug, noApiViewCollection, postsSlug } from '../shared'
+
+export const seed: Config['onInit'] = async (payload) => {
+ await payload.create({
+ collection: 'users',
+ data: {
+ email: devUser.email,
+ password: devUser.password,
+ },
+ })
+
+ await mapAsync([...Array(11)], async () => {
+ await payload.create({
+ collection: postsSlug,
+ data: {
+ title: 'Title',
+ description: 'Description',
+ },
+ })
+ })
+
+ await payload.create({
+ collection: customViews1Slug,
+ data: {
+ title: 'Custom View',
+ },
+ })
+
+ await payload.create({
+ collection: customViews2Slug,
+ data: {
+ title: 'Custom View',
+ },
+ })
+
+ await payload.create({
+ collection: 'geo',
+ data: {
+ point: [7, -7],
+ },
+ })
+
+ await payload.create({
+ collection: 'geo',
+ data: {
+ point: [5, -5],
+ },
+ })
+
+ await payload.create({
+ collection: noApiViewCollection,
+ data: {},
+ })
+}
diff --git a/test/admin/shared.ts b/test/admin/shared.ts
index b4226a2149..5005c7b9f3 100644
--- a/test/admin/shared.ts
+++ b/test/admin/shared.ts
@@ -16,8 +16,24 @@ export const noApiViewCollection = 'collection-no-api-view'
export const noApiViewGlobal = 'global-no-api-view'
+export const customViewPath = '/custom-view'
+
+export const customViewTitle = 'Custom View'
+
+export const customNestedViewPath = `${customViewPath}/nested-view`
+
+export const customNestedViewTitle = 'Custom Nested View'
+
export const customViews2Slug = 'custom-views-two'
export const customEditLabel = 'Custom Edit Label'
-export const customTabLabel = 'Custom Tab Component'
+export const customTabLabel = 'Custom Tab Label'
+
+export const customTabViewPath = '/custom-tab-component'
+
+export const customTabViewTitle = 'Custom View With Tab Component'
+
+export const customNestedTabViewPath = `${customTabViewPath}/nested-view`
+
+export const customNestedTabViewTitle = 'Custom Nested Tab View'
diff --git a/test/endpoints/config.ts b/test/endpoints/config.ts
index 815c838d32..d3f5dfd71d 100644
--- a/test/endpoints/config.ts
+++ b/test/endpoints/config.ts
@@ -22,7 +22,7 @@ export default buildConfigWithDefaults({
delete: () => true,
update: () => true,
},
- endpoints: [...(collectionEndpoints || [])],
+ endpoints: collectionEndpoints,
fields: [
{
name: 'title',
@@ -45,7 +45,7 @@ export default buildConfigWithDefaults({
globals: [
{
slug: globalSlug,
- endpoints: [...(globalEndpoints || [])],
+ endpoints: globalEndpoints,
fields: [],
},
{
@@ -60,7 +60,7 @@ export default buildConfigWithDefaults({
],
},
],
- endpoints: [...(endpoints || [])],
+ endpoints,
admin: {
webpack: (config) => {
return {