diff --git a/docs/admin/views.mdx b/docs/admin/views.mdx index 3e78a17c9..20035b1c3 100644 --- a/docs/admin/views.mdx +++ b/docs/admin/views.mdx @@ -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,7 +111,20 @@ export const MyCollectionConfig: SanitizedCollectionConfig = { components: { views: { edit: { - Component: '/path/to/MyCustomEditView', // highlight-line + 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 Note: - 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. 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)._ Note: - 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. 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)._ Note: - 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. 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 diff --git a/packages/next/src/views/Document/getViewsFromConfig.tsx b/packages/next/src/views/Document/getViewsFromConfig.tsx index db49e206b..a84e1b573 100644 --- a/packages/next/src/views/Document/getViewsFromConfig.tsx +++ b/packages/next/src/views/Document/getViewsFromConfig.tsx @@ -66,139 +66,89 @@ 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 + const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] = + routeSegments - if (EditOverride) { - CustomView = EditOverride - } - - if (!EditOverride) { - const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] = - routeSegments - - if (!docPermissions?.read?.permission) { - notFound() - } else { - // `../:id`, or `../create` - switch (routeSegments.length) { - case 3: { - switch (segment3) { - case 'create': { - if ('create' in docPermissions && docPermissions?.create?.permission) { - CustomView = { - payloadComponent: getCustomViewByKey(views, 'default'), - } - DefaultView = { - Component: DefaultEditView, - } - } else { - ErrorView = { - Component: UnauthorizedView, - } - } - break - } - - default: { + if (!docPermissions?.read?.permission) { + notFound() + } else { + // `../:id`, or `../create` + switch (routeSegments.length) { + case 3: { + switch (segment3) { + case 'create': { + if ('create' in docPermissions && docPermissions?.create?.permission) { CustomView = { payloadComponent: getCustomViewByKey(views, 'default'), } DefaultView = { Component: DefaultEditView, } - break + } else { + ErrorView = { + Component: UnauthorizedView, + } } + break } - break - } - // `../:id/api`, `../:id/preview`, `../:id/versions`, etc - case 4: { - switch (segment4) { - case 'api': { - if (collectionConfig?.admin?.hideAPIURL !== true) { - CustomView = { - payloadComponent: getCustomViewByKey(views, 'api'), - } - DefaultView = { - Component: DefaultAPIView, - } - } - break + default: { + CustomView = { + payloadComponent: getCustomViewByKey(views, 'default'), } - - case 'preview': { - if (livePreviewEnabled) { - DefaultView = { - Component: DefaultLivePreviewView, - } - } - break - } - - case 'versions': { - if (docPermissions?.readVersions?.permission) { - CustomView = { - payloadComponent: getCustomViewByKey(views, 'versions'), - } - DefaultView = { - Component: DefaultVersionsView, - } - } else { - ErrorView = { - Component: UnauthorizedView, - } - } - break - } - - default: { - const baseRoute = [ - adminRoute !== '/' && adminRoute, - 'collections', - collectionSlug, - segment3, - ] - .filter(Boolean) - .join('/') - - const currentRoute = [baseRoute, segment4, segment5, ...remainingSegments] - .filter(Boolean) - .join('/') - - CustomView = { - payloadComponent: getCustomViewByRoute({ - baseRoute, - currentRoute, - views, - }), - } - break + DefaultView = { + Component: DefaultEditView, } + break } - break } + break + } - // `../:id/versions/:version`, etc - default: { - if (segment4 === 'versions') { - if (docPermissions?.readVersions?.permission) { + // `../:id/api`, `../:id/preview`, `../:id/versions`, etc + case 4: { + switch (segment4) { + case 'api': { + if (collectionConfig?.admin?.hideAPIURL !== true) { CustomView = { - payloadComponent: getCustomViewByKey(views, 'version'), + payloadComponent: getCustomViewByKey(views, 'api'), } DefaultView = { - Component: DefaultVersionView, + Component: DefaultAPIView, + } + } + break + } + + case 'preview': { + if (livePreviewEnabled) { + DefaultView = { + Component: DefaultLivePreviewView, + } + } + break + } + + case 'versions': { + if (docPermissions?.readVersions?.permission) { + CustomView = { + payloadComponent: getCustomViewByKey(views, 'versions'), + } + DefaultView = { + Component: DefaultVersionsView, } } else { ErrorView = { Component: UnauthorizedView, } } - } else { + break + } + + default: { const baseRoute = [ adminRoute !== '/' && adminRoute, - collectionEntity, + 'collections', collectionSlug, segment3, ] @@ -216,144 +166,176 @@ export const getViewsFromConfig = ({ views, }), } + break } - break } + break + } + + // `../:id/versions/:version`, etc + default: { + if (segment4 === 'versions') { + if (docPermissions?.readVersions?.permission) { + CustomView = { + payloadComponent: getCustomViewByKey(views, 'version'), + } + DefaultView = { + Component: DefaultVersionView, + } + } else { + ErrorView = { + Component: UnauthorizedView, + } + } + } else { + const baseRoute = [ + adminRoute !== '/' && adminRoute, + collectionEntity, + collectionSlug, + segment3, + ] + .filter(Boolean) + .join('/') + + const currentRoute = [baseRoute, segment4, segment5, ...remainingSegments] + .filter(Boolean) + .join('/') + + CustomView = { + payloadComponent: getCustomViewByRoute({ + baseRoute, + currentRoute, + views, + }), + } + } + break } } } } if (globalConfig) { - const editConfig = globalConfig?.admin?.components?.views?.edit - const EditOverride = typeof editConfig === 'function' ? editConfig : null + const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments - if (EditOverride) { - CustomView = EditOverride - } - - if (!EditOverride) { - const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments - - if (!docPermissions?.read?.permission) { - notFound() - } else { - switch (routeSegments.length) { - case 2: { - CustomView = { - payloadComponent: getCustomViewByKey(views, 'default'), - } - DefaultView = { - Component: DefaultEditView, - } - break + if (!docPermissions?.read?.permission) { + notFound() + } else { + switch (routeSegments.length) { + case 2: { + CustomView = { + payloadComponent: getCustomViewByKey(views, 'default'), } - - case 3: { - // `../:slug/api`, `../:slug/preview`, `../:slug/versions`, etc - switch (segment3) { - case 'api': { - if (globalConfig?.admin?.hideAPIURL !== true) { - CustomView = { - payloadComponent: getCustomViewByKey(views, 'api'), - } - DefaultView = { - Component: DefaultAPIView, - } - } - break - } - - case 'preview': { - if (livePreviewEnabled) { - DefaultView = { - Component: DefaultLivePreviewView, - } - } - break - } - - case 'versions': { - if (docPermissions?.readVersions?.permission) { - CustomView = { - payloadComponent: getCustomViewByKey(views, 'versions'), - } - DefaultView = { - Component: DefaultVersionsView, - } - } else { - ErrorView = { - Component: UnauthorizedView, - } - } - break - } - - default: { - if (docPermissions?.read?.permission) { - const baseRoute = [adminRoute, globalEntity, globalSlug, segment3] - .filter(Boolean) - .join('/') - - const currentRoute = [baseRoute, segment3, ...remainingSegments] - .filter(Boolean) - .join('/') - - CustomView = { - payloadComponent: getCustomViewByRoute({ - baseRoute, - currentRoute, - views, - }), - } - DefaultView = { - Component: DefaultEditView, - } - } else { - ErrorView = { - Component: UnauthorizedView, - } - } - break - } - } - break + DefaultView = { + Component: DefaultEditView, } + break + } - default: { - // `../:slug/versions/:version`, etc - if (segment3 === 'versions') { - if (docPermissions?.readVersions?.permission) { + case 3: { + // `../:slug/api`, `../:slug/preview`, `../:slug/versions`, etc + switch (segment3) { + case 'api': { + if (globalConfig?.admin?.hideAPIURL !== true) { CustomView = { - payloadComponent: getCustomViewByKey(views, 'version'), + payloadComponent: getCustomViewByKey(views, 'api'), } DefaultView = { - Component: DefaultVersionView, + Component: DefaultAPIView, + } + } + break + } + + case 'preview': { + if (livePreviewEnabled) { + DefaultView = { + Component: DefaultLivePreviewView, + } + } + break + } + + case 'versions': { + if (docPermissions?.readVersions?.permission) { + CustomView = { + payloadComponent: getCustomViewByKey(views, 'versions'), + } + DefaultView = { + Component: DefaultVersionsView, } } else { ErrorView = { Component: UnauthorizedView, } } - } else { - const baseRoute = [adminRoute !== '/' && adminRoute, 'globals', globalSlug] - .filter(Boolean) - .join('/') + break + } - const currentRoute = [baseRoute, segment3, ...remainingSegments] - .filter(Boolean) - .join('/') + default: { + if (docPermissions?.read?.permission) { + const baseRoute = [adminRoute, globalEntity, globalSlug, segment3] + .filter(Boolean) + .join('/') + const currentRoute = [baseRoute, segment3, ...remainingSegments] + .filter(Boolean) + .join('/') + + CustomView = { + payloadComponent: getCustomViewByRoute({ + baseRoute, + currentRoute, + views, + }), + } + DefaultView = { + Component: DefaultEditView, + } + } else { + ErrorView = { + Component: UnauthorizedView, + } + } + break + } + } + break + } + + default: { + // `../:slug/versions/:version`, etc + if (segment3 === 'versions') { + if (docPermissions?.readVersions?.permission) { CustomView = { - payloadComponent: getCustomViewByRoute({ - baseRoute, - currentRoute, - views, - }), + payloadComponent: getCustomViewByKey(views, 'version'), + } + DefaultView = { + Component: DefaultVersionView, + } + } else { + ErrorView = { + Component: UnauthorizedView, } } - break + } else { + const baseRoute = [adminRoute !== '/' && adminRoute, 'globals', globalSlug] + .filter(Boolean) + .join('/') + + const currentRoute = [baseRoute, segment3, ...remainingSegments] + .filter(Boolean) + .join('/') + + CustomView = { + payloadComponent: getCustomViewByRoute({ + baseRoute, + currentRoute, + views, + }), + } } + break } } } diff --git a/packages/next/src/views/Document/index.tsx b/packages/next/src/views/Document/index.tsx index 7783c7702..0f49023a1 100644 --- a/packages/next/src/views/Document/index.tsx +++ b/packages/next/src/views/Document/index.tsx @@ -60,7 +60,7 @@ export const Document: React.FC = async ({ const isEditing = getIsEditing({ id, collectionSlug, globalSlug }) - let ViewOverride: MappedComponent + let RootViewOverride: MappedComponent let CustomView: MappedComponent let DefaultView: MappedComponent let ErrorView: MappedComponent @@ -115,19 +115,18 @@ export const Document: React.FC = 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 = 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 = 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 = 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 = 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 = async ({ initialState={formState} isEditing={isEditing} > - {!ViewOverride && ( + {!RootViewOverride && ( = async ({ ) : ( )} diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 4b5dc4ca7..24a0cb135 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -912,28 +912,44 @@ export type SanitizedConfig = { 'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload' > -export type EditConfig = { - [key: string]: Partial - /** - * 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` - */ - api?: Partial - default?: Partial - livePreview?: Partial - version?: Partial - versions?: Partial - // TODO: uncomment these as they are built - // references?: EditView - // relationships?: EditView -} +export type EditConfig = + | { + [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. + */ + api?: Partial + default?: Partial + livePreview?: Partial + root?: never + version?: Partial + versions?: Partial + // 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 + version?: never + versions?: never + } export type EntityDescriptionComponent = CustomComponent diff --git a/test/admin/collections/CustomViews1.ts b/test/admin/collections/CustomViews1.ts index d281ae7bb..bc965ee78 100644 --- a/test/admin/collections/CustomViews1.ts +++ b/test/admin/collections/CustomViews1.ts @@ -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', }, },