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 ### 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. For more information on customizing the Edit View, see the [Edit View](./edit-view) documentation.
@@ -107,8 +107,8 @@ export const MyCollection: CollectionConfig = {
components: { components: {
views: { views: {
edit: { edit: {
myCustomTab: { myCustomView: {
Component: '/path/to/MyCustomTab', Component: '/path/to/MyCustomView',
path: '/my-custom-tab', path: '/my-custom-tab',
// highlight-start // highlight-start
tab: { tab: {
@@ -116,7 +116,7 @@ export const MyCollection: CollectionConfig = {
}, },
// highlight-end // highlight-end
}, },
anotherCustomTab: { anotherCustomView: {
Component: '/path/to/AnotherCustomView', Component: '/path/to/AnotherCustomView',
path: '/another-custom-view', path: '/another-custom-view',
// highlight-start // 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' import { documentViewKeys } from './tabs/index.js'
export const getCustomViews = (args: { export const getCustomViews = (args: {
collectionConfig: SanitizedCollectionConfig collectionConfig: SanitizedCollectionConfig
globalConfig: SanitizedGlobalConfig globalConfig: SanitizedGlobalConfig
}): EditViewConfig[] => { }): DocumentViewConfig[] => {
const { collectionConfig, globalConfig } = args const { collectionConfig, globalConfig } = args
let customViews: EditViewConfig[] let customViews: DocumentViewConfig[]
if (collectionConfig) { if (collectionConfig) {
const collectionViewsConfig = 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: { export const getViewConfig = (args: {
collectionConfig: SanitizedCollectionConfig collectionConfig: SanitizedCollectionConfig
globalConfig: SanitizedGlobalConfig globalConfig: SanitizedGlobalConfig
name: string name: string
}): EditViewConfig => { }): DocumentViewConfig => {
const { name, collectionConfig, globalConfig } = args const { name, collectionConfig, globalConfig } = args
if (collectionConfig) { if (collectionConfig) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -340,29 +340,50 @@ export type Endpoint = {
root?: never 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 meta?: MetaConfig
} & ( tab?: DocumentTabConfig
| { }
actions?: CustomComponent[]
} /*
| { 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.
Component: EditViewComponent To render just a tab component without an accompanying view, you can omit the `path` and `Component` properties altogether.
path?: string */
} export type CustomDocumentViewConfig =
| { | ({
path?: string Component: DocumentViewComponent
/** path: string
* Add a new Edit View to the admin panel } & BaseDocumentViewConfig)
* i.e. you can render a custom view that has no tab, if desired | ({
* Or override a specific properties of an existing one Component?: DocumentViewComponent
* i.e. you can customize the `Default` view tab label, if desired path?: never
*/ } & BaseDocumentViewConfig)
tab?: DocumentTabConfig
} /*
) 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 } export type Params = { [key: string]: string | string[] | undefined }
@@ -1260,46 +1281,46 @@ export type SanitizedConfig = {
export type EditConfig = EditConfigWithoutRoot | EditConfigWithRoot 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 = { export type EditConfigWithRoot = {
api?: never api?: never
default?: never default?: never
livePreview?: never livePreview?: never
/** root: DefaultDocumentViewConfig
* 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>
version?: never version?: never
versions?: 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 = { export type EditConfigWithoutRoot = {
[key: string]: EditViewConfig [K in Exclude<string, KnownEditKeys>]: CustomDocumentViewConfig
/** } & {
* Replace or modify individual nested routes, or add new ones: api?: DefaultDocumentViewConfig
* + `default` - `/admin/collections/:collection/:id` default?: DefaultDocumentViewConfig
* + `api` - `/admin/collections/:collection/:id/api` livePreview?: DefaultDocumentViewConfig
* + `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
root?: never root?: never
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve version?: DefaultDocumentViewConfig
version?: Partial<EditViewConfig> versions?: DefaultDocumentViewConfig
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
versions?: Partial<EditViewConfig>
} }
export type EntityDescriptionComponent = CustomComponent export type EntityDescriptionComponent = CustomComponent

View File

@@ -1,5 +1,7 @@
import type { import type {
BulkOperationResult, BulkOperationResult,
CustomDocumentViewConfig,
DefaultDocumentViewConfig,
JoinQuery, JoinQuery,
PaginatedDocs, PaginatedDocs,
SelectType, 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
}>()
})
})
}) })