fix: remove unsupported path property from default document view configs (#12774)

Customizing the `path` property on default document views is currently
not supported, but the types suggest that it is. You can only provide a
path to custom views. This PR ensures that `path` cannot be set on
default views as expected.

For example:

```ts
import type { CollectionConfig } from 'payload'

export const MyCollectionConfig: CollectionConfig = {
  // ...
  admin: {
    components: {
      views: {
        edit: {
          default: {
            path: '/' // THIS IS NOT ALLOWED!
          },
          myCustomView: {
            path: '/edit', // THIS IS ALLOWED!
            Component: '/collections/CustomViews3/MyEditView.js#MyEditView',
          },
        },
      },
    },
  },
}
```

For background context, this was deeply explored in #12701. This is not
planned, however, due to [performance and maintainability
concerns](https://github.com/payloadcms/payload/pull/12701#issuecomment-2963926925),
plus [there are alternatives to achieve
this](https://github.com/payloadcms/payload/pull/12772).

This PR also fixes and improves various jsdocs, and fixes a typo found
in the docs.
This commit is contained in:
Jacob Fletcher
2025-06-12 09:01:20 -04:00
committed by GitHub
parent 143aff57ae
commit f64a0aec5f
10 changed files with 110 additions and 68 deletions

View File

@@ -88,7 +88,7 @@ export const MyCollection: CollectionConfig = {
### Edit View
The Edit View is where users interact with individual Collection and Global Documents. This is where they can view, edit, and save their content. the Edit View is keyed under the `default` property in the `views.edit` object.
The Edit View is where users interact with individual Collection and Global Documents. This is where they can view, edit, and save their content. The Edit View is keyed under the `default` property in the `views.edit` object.
For more information on customizing the Edit View, see the [Edit View](./edit-view) documentation.
@@ -107,8 +107,8 @@ export const MyCollection: CollectionConfig = {
components: {
views: {
edit: {
myCustomTab: {
Component: '/path/to/MyCustomTab',
myCustomView: {
Component: '/path/to/MyCustomView',
path: '/my-custom-tab',
// highlight-start
tab: {
@@ -116,7 +116,7 @@ export const MyCollection: CollectionConfig = {
},
// highlight-end
},
anotherCustomTab: {
anotherCustomView: {
Component: '/path/to/AnotherCustomView',
path: '/another-custom-view',
// highlight-start

View File

@@ -1,14 +1,14 @@
import type { EditViewConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
import type { DocumentViewConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
import { documentViewKeys } from './tabs/index.js'
export const getCustomViews = (args: {
collectionConfig: SanitizedCollectionConfig
globalConfig: SanitizedGlobalConfig
}): EditViewConfig[] => {
}): DocumentViewConfig[] => {
const { collectionConfig, globalConfig } = args
let customViews: EditViewConfig[]
let customViews: DocumentViewConfig[]
if (collectionConfig) {
const collectionViewsConfig =

View File

@@ -1,10 +1,10 @@
import type { EditViewConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
import type { DocumentViewConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
export const getViewConfig = (args: {
collectionConfig: SanitizedCollectionConfig
globalConfig: SanitizedGlobalConfig
name: string
}): EditViewConfig => {
}): DocumentViewConfig => {
const { name, collectionConfig, globalConfig } = args
if (collectionConfig) {

View File

@@ -98,9 +98,11 @@ export const DocumentTabs: React.FC<{
return null
})}
{customViews?.map((CustomView, index) => {
if ('tab' in CustomView) {
const { path, tab } = CustomView
{customViews?.map((customViewConfig, index) => {
if ('tab' in customViewConfig) {
const { tab } = customViewConfig
const path = 'path' in customViewConfig ? customViewConfig.path : ''
if (tab.Component) {
return RenderServerComponent({

View File

@@ -10,7 +10,7 @@ import { generateLivePreviewViewMetadata } from '../LivePreview/metadata.js'
import { generateNotFoundViewMetadata } from '../NotFound/metadata.js'
import { generateVersionViewMetadata } from '../Version/metadata.js'
import { generateVersionsViewMetadata } from '../Versions/metadata.js'
import { getViewsFromConfig } from './getViewsFromConfig.js'
import { getViewsFromConfig } from './getDocumentView.js'
export type GenerateEditViewMetadata = (
args: {

View File

@@ -16,18 +16,18 @@ import { logError } from 'payload'
import { formatAdminURL } from 'payload/shared'
import React from 'react'
import type { ViewFromConfig } from './getDocumentView.js'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
import type { ViewFromConfig } from './getViewsFromConfig.js'
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
import { NotFoundView } from '../NotFound/index.js'
import { getDocPreferences } from './getDocPreferences.js'
import { getDocumentData } from './getDocumentData.js'
import { getDocumentPermissions } from './getDocumentPermissions.js'
import { getViewsFromConfig } from './getDocumentView.js'
import { getIsLocked } from './getIsLocked.js'
import { getMetaBySegment } from './getMetaBySegment.js'
import { getVersions } from './getVersions.js'
import { getViewsFromConfig } from './getViewsFromConfig.js'
import { renderDocumentSlots } from './renderDocumentSlots.js'
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)

View File

@@ -328,10 +328,14 @@ export type CollectionAdminOptions = {
listMenuItems?: CustomComponent[]
views?: {
/**
* Set to a React component to replace the entire Edit View, including all nested routes.
* Set to an object to replace or modify individual nested routes, or to add new ones.
* Replace, modify, or add new "document" views.
* @link https://payloadcms.com/docs/custom-components/document-views
*/
edit?: EditConfig
/**
* Replace or modify the "list" view.
* @link https://payloadcms.com/docs/custom-components/list-view
*/
list?: {
actions?: CustomComponent[]
Component?: PayloadComponent

View File

@@ -340,29 +340,50 @@ export type Endpoint = {
root?: never
}
export type EditViewComponent = PayloadComponent<DocumentViewServerProps>
/**
* @deprecated
* This type will be renamed in v4.
* Use `DocumentViewComponent` instead.
*/
export type EditViewComponent = DocumentViewComponent
export type EditViewConfig = {
export type DocumentViewComponent = PayloadComponent<DocumentViewServerProps>
/**
* @deprecated
* This type will be renamed in v4.
* Use `DocumentViewConfig` instead.
*/
export type EditViewConfig = DocumentViewConfig
type BaseDocumentViewConfig = {
actions?: CustomComponent[]
meta?: MetaConfig
} & (
| {
actions?: CustomComponent[]
}
| {
Component: EditViewComponent
path?: string
}
| {
path?: string
/**
* Add a new Edit View to the admin panel
* i.e. you can render a custom view that has no tab, if desired
* Or override a specific properties of an existing one
* i.e. you can customize the `Default` view tab label, if desired
*/
tab?: DocumentTabConfig
}
)
tab?: DocumentTabConfig
}
/*
If your view does not originate from a "known" key, e.g. `myCustomView`, then it is considered a "custom" view and can accept a `path`, etc.
To render just a tab component without an accompanying view, you can omit the `path` and `Component` properties altogether.
*/
export type CustomDocumentViewConfig =
| ({
Component: DocumentViewComponent
path: string
} & BaseDocumentViewConfig)
| ({
Component?: DocumentViewComponent
path?: never
} & BaseDocumentViewConfig)
/*
If your view does originates from a "known" key, e.g. `api`, then it is considered a "default" view and cannot accept a `path`, etc.
*/
export type DefaultDocumentViewConfig = {
Component?: DocumentViewComponent
} & BaseDocumentViewConfig
export type DocumentViewConfig = CustomDocumentViewConfig | DefaultDocumentViewConfig
export type Params = { [key: string]: string | string[] | undefined }
@@ -1260,46 +1281,46 @@ export type SanitizedConfig = {
export type EditConfig = EditConfigWithoutRoot | EditConfigWithRoot
/**
* Replace or modify _all_ nested document views and routes, including the document header, controls, and tabs. This cannot be used in conjunction with other nested views.
* + `root` - `/admin/collections/:collection/:id/**\/*`
* @link https://payloadcms.com/docs/custom-components/document-views#document-root
*/
export type EditConfigWithRoot = {
api?: never
default?: never
livePreview?: never
/**
* Replace or modify _all_ nested document views and routes, including the document header, controls, and tabs. This cannot be used in conjunction with other nested views.
* + `root` - `/admin/collections/:collection/:id/**\/*`
*/
root: Partial<EditViewConfig>
root: DefaultDocumentViewConfig
version?: never
versions?: never
}
type KnownEditKeys = 'api' | 'default' | 'livePreview' | 'root' | 'version' | 'versions'
/**
* Replace or modify individual nested routes, or add new ones:
* + `default` - `/admin/collections/:collection/:id`
* + `api` - `/admin/collections/:collection/:id/api`
* + `livePreview` - `/admin/collections/:collection/:id/preview`
* + `references` - `/admin/collections/:collection/:id/references`
* + `relationships` - `/admin/collections/:collection/:id/relationships`
* + `versions` - `/admin/collections/:collection/:id/versions`
* + `version` - `/admin/collections/:collection/:id/versions/:version`
* + `customView` - `/admin/collections/:collection/:id/:path`
*
* To override the entire Edit View including all nested views, use the `root` key.
*
* @link https://payloadcms.com/docs/custom-components/document-views
*/
export type EditConfigWithoutRoot = {
[key: string]: EditViewConfig
/**
* Replace or modify individual nested routes, or add new ones:
* + `default` - `/admin/collections/:collection/:id`
* + `api` - `/admin/collections/:collection/:id/api`
* + `livePreview` - `/admin/collections/:collection/:id/preview`
* + `references` - `/admin/collections/:collection/:id/references`
* + `relationships` - `/admin/collections/:collection/:id/relationships`
* + `versions` - `/admin/collections/:collection/:id/versions`
* + `version` - `/admin/collections/:collection/:id/versions/:version`
* + `customView` - `/admin/collections/:collection/:id/:path`
*
* To override the entire Edit View including all nested views, use the `root` key.
*/
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
api?: Partial<EditViewConfig>
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
default?: Partial<EditViewConfig>
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
livePreview?: Partial<EditViewConfig>
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
[K in Exclude<string, KnownEditKeys>]: CustomDocumentViewConfig
} & {
api?: DefaultDocumentViewConfig
default?: DefaultDocumentViewConfig
livePreview?: DefaultDocumentViewConfig
root?: never
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
version?: Partial<EditViewConfig>
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
versions?: Partial<EditViewConfig>
version?: DefaultDocumentViewConfig
versions?: DefaultDocumentViewConfig
}
export type EntityDescriptionComponent = CustomComponent

View File

@@ -1,5 +1,7 @@
import type {
BulkOperationResult,
CustomDocumentViewConfig,
DefaultDocumentViewConfig,
JoinQuery,
PaginatedDocs,
SelectType,
@@ -158,4 +160,17 @@ describe('Types testing', () => {
})
})
})
describe('views', () => {
test('default view config', () => {
expect<DefaultDocumentViewConfig>().type.not.toBeAssignableWith<{
path: string
}>()
expect<CustomDocumentViewConfig>().type.toBeAssignableWith<{
Component: string
path: string
}>()
})
})
})