fix(next): supports root document view overrides as separate from default edit view (#7673)

## Description

We've since lost the ability to override the document view at the
root-level. This was a feature that made it possible to override _the
entire document routing/view structure_, including the document
header/tabs and all nested routes within, i.e. the API route/view, the
Live Preview route/view, etc. This is distinct from the "default" edit
view, which _only_ targets the component rendered within the "edit" tab.
This regression was introduced when types were simplified down to better
support "component paths" here: #7620. The `default` key was incorrectly
used as the "root" view override. To continue to support stricter types
_and_ root view overrides, a new `root` key has been added to the
`views` config.

You were previously able to do this:

```tsx
import { MyComponent } from './MyComponent.js'

export const MyCollection = {
  // ...
  admin: {
    views: {
      Edit: MyComponent
    }
  }
}
```

This is now done like this:

```tsx
export const MyCollection = {
  // ...
  admin: {
    views: {
      edit: {
        root: {
          Component: './path-to-my-component.js'
        }
      }
    }
  }
}
```

Some of the documentation was also incorrect according to the new
component paths API.

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] This change requires a documentation update

## Checklist:

- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
This commit is contained in:
Jacob Fletcher
2024-08-14 16:02:14 -04:00
committed by GitHub
parent 4f323a3754
commit a212cdef3f
5 changed files with 303 additions and 269 deletions

View File

@@ -53,7 +53,7 @@ For more granular control, pass a configuration object instead. Payload exposes
| Property | Description | | Property | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`Component`** \* | Pass in the component that should be rendered when a user navigates to this route. | | **`Component`** \* | Pass in the component path that should be rendered when a user navigates to this route. |
| **`path`** \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. | | **`path`** \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
| **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. | | **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
| **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. | | **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
@@ -111,8 +111,21 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
components: { components: {
views: { views: {
edit: { edit: {
root: {
Component: '/path/to/MyCustomEditView', // highlight-line Component: '/path/to/MyCustomEditView', // highlight-line
} }
// other options include:
// default
// versions
// version
// api
// livePreview
// [key: string]
// See "Document Views" for more details
},
list: {
Component: '/path/to/MyCustomListView',
}
}, },
}, },
}, },
@@ -123,7 +136,7 @@ _For details on how to build Custom Views, see [Building Custom Views](#building
<Banner type="warning"> <Banner type="warning">
<strong>Note:</strong> <strong>Note:</strong>
The `Edit` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `Edit.Default` key instead. The `root` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `edit.default` key instead.
</Banner> </Banner>
The following options are available: The following options are available:
@@ -152,18 +165,29 @@ export const MyGlobalConfig: SanitizedGlobalConfig = {
admin: { admin: {
components: { components: {
views: { views: {
edit: '/path/to/MyCustomEditView', // highlight-line edit: {
root: {
Component: '/path/to/MyCustomEditView', // highlight-line
}
// other options include:
// default
// versions
// version
// api
// livePreview
// [key: string]
}, },
}, },
}, },
}) },
}
``` ```
_For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._ _For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._
<Banner type="warning"> <Banner type="warning">
<strong>Note:</strong> <strong>Note:</strong>
The `Edit` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `Edit.Default` key instead. The `root` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `edit.default` key instead.
</Banner> </Banner>
The following options are available: The following options are available:
@@ -199,25 +223,26 @@ export const MyCollectionOrGlobalConfig: SanitizedCollectionConfig = {
}, },
}, },
}, },
}) }
``` ```
_For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._ _For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._
<Banner type="warning"> <Banner type="warning">
<strong>Note:</strong> <strong>Note:</strong>
If you need to replace the _entire_ Edit View, including _all_ nested Document Views, use the `Edit` key itself. See [Custom Collection Views](#collection-views) or [Custom Global Views](#global-views) for more information. If you need to replace the _entire_ Edit View, including _all_ nested Document Views, use the `root` key. See [Custom Collection Views](#collection-views) or [Custom Global Views](#global-views) for more information.
</Banner> </Banner>
The following options are available: The following options are available:
| Property | Description | | Property | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- | | ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **`default`** | The Default view is the primary view in which your document is edited. | | **`root`** | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. |
| **`versions`** | The Versions view is used to view the version history of a single document. [More details](../versions). | | **`default`** | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. |
| **`version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). | | **`versions`** | The Versions View is used to navigate the version history of a single document. It is rendered within the "Versions" tab. [More details](../versions). |
| **`api`** | The API view is used to display the REST API JSON response for a given document. | | **`version`** | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](../versions). |
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). | | **`api`** | The API View is used to display the REST API JSON response for a given document. It is rendered within the "API" tab. |
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. It is rendered within the "Live Preview" tab. [More details](../live-preview). |
### Document Tabs ### Document Tabs

View File

@@ -66,14 +66,6 @@ export const getViewsFromConfig = ({
config?.admin?.livePreview?.globals?.includes(globalConfig?.slug) config?.admin?.livePreview?.globals?.includes(globalConfig?.slug)
if (collectionConfig) { if (collectionConfig) {
const editConfig = collectionConfig?.admin?.components?.views?.edit
const EditOverride = typeof editConfig === 'function' ? editConfig : null
if (EditOverride) {
CustomView = EditOverride
}
if (!EditOverride) {
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] = const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
routeSegments routeSegments
@@ -222,17 +214,8 @@ export const getViewsFromConfig = ({
} }
} }
} }
}
if (globalConfig) { if (globalConfig) {
const editConfig = globalConfig?.admin?.components?.views?.edit
const EditOverride = typeof editConfig === 'function' ? editConfig : null
if (EditOverride) {
CustomView = EditOverride
}
if (!EditOverride) {
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
if (!docPermissions?.read?.permission) { if (!docPermissions?.read?.permission) {
@@ -357,7 +340,6 @@ export const getViewsFromConfig = ({
} }
} }
} }
}
return { return {
CustomView, CustomView,

View File

@@ -60,7 +60,7 @@ export const Document: React.FC<AdminViewProps> = async ({
const isEditing = getIsEditing({ id, collectionSlug, globalSlug }) const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
let ViewOverride: MappedComponent<ServerSideEditViewProps> let RootViewOverride: MappedComponent<ServerSideEditViewProps>
let CustomView: MappedComponent<ServerSideEditViewProps> let CustomView: MappedComponent<ServerSideEditViewProps>
let DefaultView: MappedComponent<ServerSideEditViewProps> let DefaultView: MappedComponent<ServerSideEditViewProps>
let ErrorView: MappedComponent<AdminViewProps> let ErrorView: MappedComponent<AdminViewProps>
@@ -115,19 +115,18 @@ export const Document: React.FC<AdminViewProps> = async ({
apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}${apiQueryParams}` apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}${apiQueryParams}`
ViewOverride = RootViewOverride =
collectionConfig?.admin?.components?.views?.edit?.default && collectionConfig?.admin?.components?.views?.edit?.root &&
'Component' in collectionConfig.admin.components.views.edit.default 'Component' in collectionConfig.admin.components.views.edit.root
? createMappedComponent( ? createMappedComponent(
collectionConfig?.admin?.components?.views?.edit?.default collectionConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
undefined, undefined,
undefined, undefined,
'collectionConfig?.admin?.components?.views?.edit?.default', 'collectionConfig?.admin?.components?.views?.edit?.root',
) )
: null : null
if (!ViewOverride) { if (!RootViewOverride) {
const collectionViews = getViewsFromConfig({ const collectionViews = getViewsFromConfig({
collectionConfig, collectionConfig,
config, config,
@@ -157,7 +156,7 @@ export const Document: React.FC<AdminViewProps> = async ({
) )
} }
if (!CustomView && !DefaultView && !ViewOverride && !ErrorView) { if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView') ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
} }
} }
@@ -170,9 +169,11 @@ export const Document: React.FC<AdminViewProps> = async ({
const params = new URLSearchParams({ const params = new URLSearchParams({
locale: locale?.code, locale: locale?.code,
}) })
if (globalConfig.versions?.drafts) { if (globalConfig.versions?.drafts) {
params.append('draft', 'true') params.append('draft', 'true')
} }
if (locale?.code) { if (locale?.code) {
params.append('locale', locale.code) params.append('locale', locale.code)
} }
@@ -181,10 +182,18 @@ export const Document: React.FC<AdminViewProps> = async ({
apiURL = `${serverURL}${apiRoute}/${globalSlug}${apiQueryParams}` apiURL = `${serverURL}${apiRoute}/${globalSlug}${apiQueryParams}`
const editConfig = globalConfig?.admin?.components?.views?.edit RootViewOverride =
ViewOverride = typeof editConfig === 'function' ? editConfig : null globalConfig?.admin?.components?.views?.edit?.root &&
'Component' in globalConfig.admin.components.views.edit.root
? createMappedComponent(
globalConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
undefined,
undefined,
'globalConfig?.admin?.components?.views?.edit?.root',
)
: null
if (!ViewOverride) { if (!RootViewOverride) {
const globalViews = getViewsFromConfig({ const globalViews = getViewsFromConfig({
config, config,
docPermissions, docPermissions,
@@ -213,7 +222,7 @@ export const Document: React.FC<AdminViewProps> = async ({
'globalViews?.ErrorView.payloadComponent', 'globalViews?.ErrorView.payloadComponent',
) )
if (!CustomView && !DefaultView && !ViewOverride && !ErrorView) { if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView') ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
} }
} }
@@ -268,7 +277,7 @@ export const Document: React.FC<AdminViewProps> = async ({
initialState={formState} initialState={formState}
isEditing={isEditing} isEditing={isEditing}
> >
{!ViewOverride && ( {!RootViewOverride && (
<DocumentHeader <DocumentHeader
collectionConfig={collectionConfig} collectionConfig={collectionConfig}
globalConfig={globalConfig} globalConfig={globalConfig}
@@ -294,7 +303,9 @@ export const Document: React.FC<AdminViewProps> = async ({
<RenderComponent mappedComponent={ErrorView} /> <RenderComponent mappedComponent={ErrorView} />
) : ( ) : (
<RenderComponent <RenderComponent
mappedComponent={ViewOverride ? ViewOverride : CustomView ? CustomView : DefaultView} mappedComponent={
RootViewOverride ? RootViewOverride : CustomView ? CustomView : DefaultView
}
/> />
)} )}
</EditDepthProvider> </EditDepthProvider>

View File

@@ -912,8 +912,9 @@ export type SanitizedConfig = {
'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload' 'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
> >
export type EditConfig = { export type EditConfig =
[key: string]: Partial<EditViewConfig> | {
[key: string]: EditViewConfig
/** /**
* Replace or modify individual nested routes, or add new ones: * Replace or modify individual nested routes, or add new ones:
* + `default` - `/admin/collections/:collection/:id` * + `default` - `/admin/collections/:collection/:id`
@@ -924,16 +925,31 @@ export type EditConfig = {
* + `versions` - `/admin/collections/:collection/:id/versions` * + `versions` - `/admin/collections/:collection/:id/versions`
* + `version` - `/admin/collections/:collection/:id/versions/:version` * + `version` - `/admin/collections/:collection/:id/versions/:version`
* + `customView` - `/admin/collections/:collection/:id/:path` * + `customView` - `/admin/collections/:collection/:id/:path`
*
* To override the entire Edit View including all nested views, use the `root` key.
*/ */
api?: Partial<EditViewConfig> api?: Partial<EditViewConfig>
default?: Partial<EditViewConfig> default?: Partial<EditViewConfig>
livePreview?: Partial<EditViewConfig> livePreview?: Partial<EditViewConfig>
root?: never
version?: Partial<EditViewConfig> version?: Partial<EditViewConfig>
versions?: Partial<EditViewConfig> versions?: Partial<EditViewConfig>
// TODO: uncomment these as they are built // TODO: uncomment these as they are built
// references?: EditView // references?: EditView
// relationships?: EditView // relationships?: EditView
} }
| {
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>
version?: never
versions?: never
}
export type EntityDescriptionComponent = CustomComponent export type EntityDescriptionComponent = CustomComponent

View File

@@ -10,7 +10,7 @@ export const CustomViews1: CollectionConfig = {
// This will override the entire Edit View including all nested views, i.e. `/edit/:id/*` // This will override the entire Edit View including all nested views, i.e. `/edit/:id/*`
// To override one specific nested view, use the nested view's slug as the key // To override one specific nested view, use the nested view's slug as the key
edit: { edit: {
default: { root: {
Component: '/components/views/CustomEdit/index.js#CustomEditView', Component: '/components/views/CustomEdit/index.js#CustomEditView',
}, },
}, },