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 |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`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. |
| **`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. |
@@ -111,8 +111,21 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
components: {
views: {
edit: {
root: {
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">
<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>
The following options are available:
@@ -152,18 +165,29 @@ export const MyGlobalConfig: SanitizedGlobalConfig = {
admin: {
components: {
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)._
<Banner type="warning">
<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>
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)._
<Banner type="warning">
<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>
The following options are available:
| Property | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **`default`** | The Default view is the primary view in which your document is edited. |
| **`versions`** | The Versions view is used to view the version history of a single document. [More details](../versions). |
| **`version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). |
| **`api`** | The API view is used to display the REST API JSON response for a given document. |
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). |
| **`root`** | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. |
| **`default`** | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. |
| **`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). |
| **`version`** | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](../versions). |
| **`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

View File

@@ -66,14 +66,6 @@ export const getViewsFromConfig = ({
config?.admin?.livePreview?.globals?.includes(globalConfig?.slug)
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] =
routeSegments
@@ -222,17 +214,8 @@ export const getViewsFromConfig = ({
}
}
}
}
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
if (!docPermissions?.read?.permission) {
@@ -357,7 +340,6 @@ export const getViewsFromConfig = ({
}
}
}
}
return {
CustomView,

View File

@@ -60,7 +60,7 @@ export const Document: React.FC<AdminViewProps> = async ({
const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
let ViewOverride: MappedComponent<ServerSideEditViewProps>
let RootViewOverride: MappedComponent<ServerSideEditViewProps>
let CustomView: MappedComponent<ServerSideEditViewProps>
let DefaultView: MappedComponent<ServerSideEditViewProps>
let ErrorView: MappedComponent<AdminViewProps>
@@ -115,19 +115,18 @@ export const Document: React.FC<AdminViewProps> = async ({
apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}${apiQueryParams}`
ViewOverride =
collectionConfig?.admin?.components?.views?.edit?.default &&
'Component' in collectionConfig.admin.components.views.edit.default
RootViewOverride =
collectionConfig?.admin?.components?.views?.edit?.root &&
'Component' in collectionConfig.admin.components.views.edit.root
? createMappedComponent(
collectionConfig?.admin?.components?.views?.edit?.default
?.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.
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.
undefined,
undefined,
'collectionConfig?.admin?.components?.views?.edit?.default',
'collectionConfig?.admin?.components?.views?.edit?.root',
)
: null
if (!ViewOverride) {
if (!RootViewOverride) {
const collectionViews = getViewsFromConfig({
collectionConfig,
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')
}
}
@@ -170,9 +169,11 @@ export const Document: React.FC<AdminViewProps> = async ({
const params = new URLSearchParams({
locale: locale?.code,
})
if (globalConfig.versions?.drafts) {
params.append('draft', 'true')
}
if (locale?.code) {
params.append('locale', locale.code)
}
@@ -181,10 +182,18 @@ export const Document: React.FC<AdminViewProps> = async ({
apiURL = `${serverURL}${apiRoute}/${globalSlug}${apiQueryParams}`
const editConfig = globalConfig?.admin?.components?.views?.edit
ViewOverride = typeof editConfig === 'function' ? editConfig : null
RootViewOverride =
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({
config,
docPermissions,
@@ -213,7 +222,7 @@ export const Document: React.FC<AdminViewProps> = async ({
'globalViews?.ErrorView.payloadComponent',
)
if (!CustomView && !DefaultView && !ViewOverride && !ErrorView) {
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
}
}
@@ -268,7 +277,7 @@ export const Document: React.FC<AdminViewProps> = async ({
initialState={formState}
isEditing={isEditing}
>
{!ViewOverride && (
{!RootViewOverride && (
<DocumentHeader
collectionConfig={collectionConfig}
globalConfig={globalConfig}
@@ -294,7 +303,9 @@ export const Document: React.FC<AdminViewProps> = async ({
<RenderComponent mappedComponent={ErrorView} />
) : (
<RenderComponent
mappedComponent={ViewOverride ? ViewOverride : CustomView ? CustomView : DefaultView}
mappedComponent={
RootViewOverride ? RootViewOverride : CustomView ? CustomView : DefaultView
}
/>
)}
</EditDepthProvider>

View File

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

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/*`
// To override one specific nested view, use the nested view's slug as the key
edit: {
default: {
root: {
Component: '/components/views/CustomEdit/index.js#CustomEditView',
},
},