chore: adds e2e tests for nested views (#3962)

This commit is contained in:
Jacob Fletcher
2023-11-02 13:13:29 -04:00
committed by GitHub
parent 6b1b4ffd27
commit f7d4c04f65
14 changed files with 337 additions and 102 deletions

View File

@@ -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 `<Route />` component in [React Router v5](https://v5.reactrouter.com): For more granular control, pass a configuration object instead. Each view corresponds to its own `<Route />` component in [React Router v5](https://v5.reactrouter.com). Payload exposes all of the properties of React Router:
| Property | Description | | Property | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
@@ -129,6 +129,12 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob
} }
``` ```
<Banner type="warning">
<strong>Note:</strong>
<br />
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.
</Banner>
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._ _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). For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).

View File

@@ -1,9 +1,17 @@
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
import CustomTabComponent from '../components/CustomTabComponent' 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 CustomVersionsView from '../components/views/CustomVersions'
import CustomView from '../components/views/CustomView' import {
import { customEditLabel, customTabLabel, customViews2Slug } from '../shared' customEditLabel,
customNestedTabViewPath,
customTabLabel,
customTabViewPath,
customViews2Slug,
} from '../shared'
export const CustomViews2: CollectionConfig = { export const CustomViews2: CollectionConfig = {
slug: customViews2Slug, slug: customViews2Slug,
@@ -21,17 +29,21 @@ export const CustomViews2: CollectionConfig = {
Versions: CustomVersionsView, Versions: CustomVersionsView,
MyCustomView: { MyCustomView: {
path: '/custom-tab-view', path: '/custom-tab-view',
Component: CustomView, Component: CustomTabView,
Tab: { Tab: {
label: customTabLabel, label: customTabLabel,
href: '/custom-tab-view', href: '/custom-tab-view',
}, },
}, },
MyCustomViewWithCustomTab: { MyCustomViewWithCustomTab: {
path: '/custom-tab-component', path: customTabViewPath,
Component: CustomView, Component: CustomTabView2,
Tab: CustomTabComponent, Tab: CustomTabComponent,
}, },
MyCustomViewWithNestedPath: {
path: customNestedTabViewPath,
Component: CustomNestedTabView,
},
}, },
}, },
}, },

View File

@@ -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 (
<Fragment>
<div
style={{
marginTop: 'calc(var(--base) * 2)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1 id="custom-view-title">{customTabViewTitle}</h1>
<p>This custom view was added through the Payload config:</p>
<ul>
<li>
<code>components.views[key].Component</code>
</li>
</ul>
</div>
</Fragment>
)
}
export default CustomTabView

View File

@@ -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 (
<Fragment>
<div
style={{
marginTop: 'calc(var(--base) * 2)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1 id="custom-view-title">{customTabViewTitle}</h1>
<p>This custom view was added through the Payload config:</p>
<ul>
<li>
<code>components.views[key].Component</code>
</li>
</ul>
</div>
</Fragment>
)
}
export default CustomTabView2

View File

@@ -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 (
<Fragment>
<div
style={{
marginTop: 'calc(var(--base) * 2)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1 id="custom-view-title">{customNestedTabViewTitle}</h1>
<p>This custom view was added through the Payload config:</p>
<ul>
<li>
<code>components.views[key].Component</code>
</li>
</ul>
</div>
</Fragment>
)
}
export default CustomNestedTabView

View File

@@ -1,24 +1,10 @@
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 { type AdminViewComponent } from '../../../../../packages/payload/src/config/types'
import { customViewTitle } from '../../../shared'
const CustomView: AdminViewComponent = () => { 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 ( return (
<Fragment>
<div <div
style={{ style={{
marginTop: 'calc(var(--base) * 2)', marginTop: 'calc(var(--base) * 2)',
@@ -26,7 +12,7 @@ const CustomView: AdminViewComponent = () => {
paddingRight: 'var(--gutter-h)', paddingRight: 'var(--gutter-h)',
}} }}
> >
<h1>Custom View</h1> <h1 id="custom-view-title">{customViewTitle}</h1>
<p>This custom view was added through the Payload config:</p> <p>This custom view was added through the Payload config:</p>
<ul> <ul>
<li> <li>
@@ -34,7 +20,6 @@ const CustomView: AdminViewComponent = () => {
</li> </li>
</ul> </ul>
</div> </div>
</Fragment>
) )
} }

View File

@@ -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 (
<div
style={{
marginTop: 'calc(var(--base) * 2)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1 id="custom-view-title">{customNestedViewTitle}</h1>
<p>This custom view was added through the Payload config:</p>
<ul>
<li>
<code>components.views[key].Component</code>
</li>
</ul>
</div>
)
}
export default CustomNestedView

View File

@@ -1,9 +1,7 @@
import path from 'path' import path from 'path'
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import { devUser } from '../credentials' import { CustomViews1 } from './collections/CustomViews1'
import { CustomViews1, customViews1Slug } from './collections/CustomViews1'
import { CustomViews2 } from './collections/CustomViews2' import { CustomViews2 } from './collections/CustomViews2'
import { Geo } from './collections/Geo' import { Geo } from './collections/Geo'
import { CollectionGroup1A } from './collections/Group1A' import { CollectionGroup1A } from './collections/Group1A'
@@ -20,6 +18,8 @@ import BeforeLogin from './components/BeforeLogin'
import Logout from './components/Logout' import Logout from './components/Logout'
import CustomDefaultView from './components/views/CustomDefault' import CustomDefaultView from './components/views/CustomDefault'
import CustomMinimalRoute from './components/views/CustomMinimal' import CustomMinimalRoute from './components/views/CustomMinimal'
import CustomView from './components/views/CustomView'
import CustomNestedView from './components/views/CustomViewNested'
import { CustomGlobalViews1 } from './globals/CustomViews1' import { CustomGlobalViews1 } from './globals/CustomViews1'
import { CustomGlobalViews2 } from './globals/CustomViews2' import { CustomGlobalViews2 } from './globals/CustomViews2'
import { Global } from './globals/Global' import { Global } from './globals/Global'
@@ -27,7 +27,8 @@ import { GlobalGroup1A } from './globals/Group1A'
import { GlobalGroup1B } from './globals/Group1B' import { GlobalGroup1B } from './globals/Group1B'
import { GlobalHidden } from './globals/Hidden' import { GlobalHidden } from './globals/Hidden'
import { GlobalNoApiView } from './globals/NoApiView' import { GlobalNoApiView } from './globals/NoApiView'
import { customViews2Slug, noApiViewCollection, postsSlug } from './shared' import { seed } from './seed'
import { customNestedViewPath, customViewPath } from './shared'
export interface Post { export interface Post {
createdAt: Date createdAt: Date
@@ -51,14 +52,23 @@ export default buildConfigWithDefaults({
views: { views: {
// Dashboard: CustomDashboardView, // Dashboard: CustomDashboardView,
// Account: CustomAccountView, // Account: CustomAccountView,
CustomMinimalRoute: { CustomMinimalView: {
path: '/custom-minimal-view', path: '/custom-minimal-view',
Component: CustomMinimalRoute, Component: CustomMinimalRoute,
}, },
CustomDefaultRoute: { CustomDefaultView: {
path: '/custom-default-view', path: '/custom-default-view',
Component: CustomDefaultView, Component: CustomDefaultView,
}, },
CustomView: {
path: customViewPath,
exact: true,
Component: CustomView,
},
CustomNestedView: {
path: customNestedViewPath,
Component: CustomNestedView,
},
}, },
}, },
}, },
@@ -97,56 +107,5 @@ export default buildConfigWithDefaults({
GlobalGroup1A, GlobalGroup1A,
GlobalGroup1B, GlobalGroup1B,
], ],
onInit: async (payload) => { onInit: seed,
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: {},
})
},
}) })

View File

@@ -22,7 +22,15 @@ import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers' import { initPayloadE2E } from '../helpers/configHelpers'
import { import {
customEditLabel, customEditLabel,
customNestedTabViewPath,
customNestedTabViewTitle,
customNestedViewPath,
customNestedViewTitle,
customTabLabel, customTabLabel,
customTabViewPath,
customTabViewTitle,
customViewPath,
customViewTitle,
customViews2Slug, customViews2Slug,
globalSlug, globalSlug,
group1Collection1Slug, group1Collection1Slug,
@@ -164,6 +172,44 @@ describe('admin', () => {
await expect(page.locator('.not-found')).toContainText('Nothing found') 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 () => { test('collection - should render custom tab label', async () => {
await page.goto(customViewsURL.create) await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test') await page.locator('#field-title').fill('Test')

View File

@@ -1,9 +1,9 @@
import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types'
import CustomTabComponent from '../components/CustomTabComponent' 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 CustomVersionsView from '../components/views/CustomVersions'
import CustomView from '../components/views/CustomView'
export const CustomGlobalViews2: GlobalConfig = { export const CustomGlobalViews2: GlobalConfig = {
slug: 'custom-global-views-two', slug: 'custom-global-views-two',

59
test/admin/seed/index.ts Normal file
View File

@@ -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: {},
})
}

View File

@@ -16,8 +16,24 @@ export const noApiViewCollection = 'collection-no-api-view'
export const noApiViewGlobal = 'global-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 customViews2Slug = 'custom-views-two'
export const customEditLabel = 'Custom Edit Label' 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'

View File

@@ -22,7 +22,7 @@ export default buildConfigWithDefaults({
delete: () => true, delete: () => true,
update: () => true, update: () => true,
}, },
endpoints: [...(collectionEndpoints || [])], endpoints: collectionEndpoints,
fields: [ fields: [
{ {
name: 'title', name: 'title',
@@ -45,7 +45,7 @@ export default buildConfigWithDefaults({
globals: [ globals: [
{ {
slug: globalSlug, slug: globalSlug,
endpoints: [...(globalEndpoints || [])], endpoints: globalEndpoints,
fields: [], fields: [],
}, },
{ {
@@ -60,7 +60,7 @@ export default buildConfigWithDefaults({
], ],
}, },
], ],
endpoints: [...(endpoints || [])], endpoints,
admin: { admin: {
webpack: (config) => { webpack: (config) => {
return { return {