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 {