fix!: handles custom collection description components (#7789)
## Description Closes #7784 by properly handling custom collection description components via `admin.components.Description`. This component was incorrectly added to the `admin.components.edit` key, and also was never handled on the front-end. This was especially misleading because the client-side config had a duplicative key in the proper position. ## Breaking Changes This PR is only labeled as a breaking change because the key has changed position within the config. If you were previously defining a custom description component on a collection, simply move it into the correct position: Old: ```ts { admin: { components: { edit: { Description: '' } } } } ``` New: ```ts { admin: { components: { Description: '' } } } ``` - [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] I have added tests that prove my fix is effective or that my feature works - [x] Existing test suite passes locally with my changes - [x] I have made corresponding changes to the documentation
This commit is contained in:
@@ -31,7 +31,7 @@ The following options are available:
|
||||
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
|
||||
| **`hooks`** | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
|
||||
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. |
|
||||
| **`description`** | Text or React component to display below the Collection label in the List View to give editors more information. |
|
||||
| **`description`** | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#components). |
|
||||
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
|
||||
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this Collection. |
|
||||
| **`enableRichTextLink`** | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
@@ -69,7 +69,8 @@ The following options are available:
|
||||
| **`beforeList`** | An array of components to inject _before_ the built-in List View |
|
||||
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
|
||||
| **`afterList`** | An array of components to inject _after_ the built-in List View |
|
||||
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
|
||||
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table
|
||||
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
|
||||
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
|
||||
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
|
||||
| **`edit.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ListSelection,
|
||||
Pagination,
|
||||
PerPage,
|
||||
PopupList,
|
||||
PublishMany,
|
||||
RelationshipProvider,
|
||||
RenderComponent,
|
||||
@@ -124,13 +123,9 @@ export const DefaultListView: React.FC = () => {
|
||||
{!smallBreak && (
|
||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||
)}
|
||||
{description && (
|
||||
{(description || Description) && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<RenderComponent
|
||||
Component={ViewDescription}
|
||||
clientProps={{ description }}
|
||||
mappedComponent={Description}
|
||||
/>
|
||||
<ViewDescription Description={Description} description={description} />
|
||||
</div>
|
||||
)}
|
||||
</ListHeader>
|
||||
|
||||
@@ -85,6 +85,7 @@ export async function generateImportMap(
|
||||
if (shouldLog) {
|
||||
console.log('Generating import map')
|
||||
}
|
||||
|
||||
const importMap: InternalImportMap = {}
|
||||
const imports: Imports = {}
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ export function iterateCollections({
|
||||
addToImportMap(collection.admin?.components?.afterListTable)
|
||||
addToImportMap(collection.admin?.components?.beforeList)
|
||||
addToImportMap(collection.admin?.components?.beforeListTable)
|
||||
addToImportMap(collection.admin?.components?.Description)
|
||||
|
||||
addToImportMap(collection.admin?.components?.edit?.Description)
|
||||
addToImportMap(collection.admin?.components?.edit?.PreviewButton)
|
||||
addToImportMap(collection.admin?.components?.edit?.PublishButton)
|
||||
addToImportMap(collection.admin?.components?.edit?.SaveButton)
|
||||
|
||||
@@ -24,7 +24,6 @@ export type ClientCollectionConfig = {
|
||||
beforeList: MappedComponent[]
|
||||
beforeListTable: MappedComponent[]
|
||||
edit: {
|
||||
Description: MappedComponent
|
||||
PreviewButton: MappedComponent
|
||||
PublishButton: MappedComponent
|
||||
SaveButton: MappedComponent
|
||||
|
||||
@@ -250,6 +250,7 @@ export type CollectionAdminOptions = {
|
||||
* Custom admin components
|
||||
*/
|
||||
components?: {
|
||||
Description?: EntityDescriptionComponent
|
||||
afterList?: CustomComponent[]
|
||||
afterListTable?: CustomComponent[]
|
||||
beforeList?: CustomComponent[]
|
||||
@@ -258,8 +259,6 @@ export type CollectionAdminOptions = {
|
||||
* Components within the edit view
|
||||
*/
|
||||
edit?: {
|
||||
Description?: EntityDescriptionComponent
|
||||
|
||||
/**
|
||||
* Replaces the "Preview" button
|
||||
*/
|
||||
|
||||
@@ -251,9 +251,13 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
{selectedCollectionConfig?.admin?.description && (
|
||||
{(selectedCollectionConfig?.admin?.description ||
|
||||
selectedCollectionConfig?.admin?.components?.Description) && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={selectedCollectionConfig.admin.description} />
|
||||
<ViewDescription
|
||||
Description={selectedCollectionConfig.admin?.components?.Description}
|
||||
description={selectedCollectionConfig.admin?.description}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{moreThanOneAvailableCollection && (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
import type { DescriptionFunction, StaticDescription } from 'payload'
|
||||
import type { DescriptionFunction, MappedComponent, StaticDescription } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import { RenderComponent } from '../../providers/Config/RenderComponent.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -12,7 +13,8 @@ export type ViewDescriptionComponent = React.ComponentType<any>
|
||||
type Description = DescriptionFunction | StaticDescription | ViewDescriptionComponent | string
|
||||
|
||||
export type ViewDescriptionProps = {
|
||||
readonly description?: Description
|
||||
readonly Description?: MappedComponent
|
||||
readonly description?: StaticDescription
|
||||
}
|
||||
|
||||
export function isComponent(description: Description): description is ViewDescriptionComponent {
|
||||
@@ -21,11 +23,10 @@ export function isComponent(description: Description): description is ViewDescri
|
||||
|
||||
export const ViewDescription: React.FC<ViewDescriptionProps> = (props) => {
|
||||
const { i18n } = useTranslation()
|
||||
const { description } = props
|
||||
const { Description, description, ...rest } = props
|
||||
|
||||
if (isComponent(description)) {
|
||||
const Description = description
|
||||
return <Description />
|
||||
if (Description) {
|
||||
return <RenderComponent clientProps={{ description, ...rest }} mappedComponent={Description} />
|
||||
}
|
||||
|
||||
if (description) {
|
||||
|
||||
@@ -224,21 +224,19 @@ export const createClientCollectionConfig = ({
|
||||
|
||||
clientCollection.admin.description = description
|
||||
|
||||
if (collection.admin.components.edit?.Description) {
|
||||
clientCollection.admin.components.edit.Description = createMappedComponent(
|
||||
collection.admin.components.edit.Description,
|
||||
if (collection.admin.components?.Description) {
|
||||
clientCollection.admin.components.Description = createMappedComponent(
|
||||
collection.admin.components.Description,
|
||||
{
|
||||
clientProps: {
|
||||
description,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
'collection.admin.components.edit.Description',
|
||||
'collection.admin.components.Description',
|
||||
)
|
||||
}
|
||||
|
||||
clientCollection.admin.description = description
|
||||
|
||||
clientCollection.admin.components.views = (
|
||||
collection?.admin?.components?.views
|
||||
? deepCopyObjectSimple(collection?.admin?.components?.views)
|
||||
|
||||
@@ -6,6 +6,7 @@ export const CustomViews1: CollectionConfig = {
|
||||
slug: customViews1CollectionSlug,
|
||||
admin: {
|
||||
components: {
|
||||
Description: '/components/ViewDescription/index.js#ViewDescription',
|
||||
views: {
|
||||
// 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
|
||||
|
||||
@@ -9,7 +9,7 @@ export const Posts: CollectionConfig = {
|
||||
slug: postsCollectionSlug,
|
||||
admin: {
|
||||
defaultColumns: ['id', 'number', 'title', 'description', 'demoUIField'],
|
||||
description: 'Description',
|
||||
description: 'This is a custom collection description.',
|
||||
group: 'One',
|
||||
listSearchableFields: ['id', 'title', 'description', 'number'],
|
||||
meta: {
|
||||
|
||||
7
test/admin/components/ViewDescription/index.tsx
Normal file
7
test/admin/components/ViewDescription/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const ViewDescription: React.FC = () => {
|
||||
return <p className="view-description">This is a custom view description component.</p>
|
||||
}
|
||||
@@ -559,7 +559,7 @@ describe('admin1', () => {
|
||||
await expect(page.locator('#custom-server-field-label')).toBeVisible()
|
||||
})
|
||||
|
||||
test('renders custom description component', async () => {
|
||||
test('renders custom field description text', async () => {
|
||||
await page.goto(customFieldsURL.create)
|
||||
await page.waitForURL(customFieldsURL.create)
|
||||
await expect(page.locator('#custom-client-field-description')).toBeVisible()
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { customAdminRoutes } from '../../shared.js'
|
||||
import { geoCollectionSlug, postsCollectionSlug } from '../../slugs.js'
|
||||
import { customViews1CollectionSlug, geoCollectionSlug, postsCollectionSlug } from '../../slugs.js'
|
||||
|
||||
const { beforeAll, beforeEach, describe } = test
|
||||
|
||||
@@ -42,6 +42,7 @@ describe('admin2', () => {
|
||||
let page: Page
|
||||
let geoUrl: AdminUrlUtil
|
||||
let postsUrl: AdminUrlUtil
|
||||
let customViewsUrl: AdminUrlUtil
|
||||
|
||||
let serverURL: string
|
||||
let adminRoutes: ReturnType<typeof getRoutes>
|
||||
@@ -56,8 +57,10 @@ describe('admin2', () => {
|
||||
dirname,
|
||||
prebuild,
|
||||
}))
|
||||
|
||||
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
|
||||
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
|
||||
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -106,6 +109,26 @@ describe('admin2', () => {
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2)
|
||||
})
|
||||
|
||||
describe('list view descriptions', () => {
|
||||
test('should render static collection descriptions', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
await expect(
|
||||
page.locator('.view-description', {
|
||||
hasText: exactText('This is a custom collection description.'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should render dynamic collection description components', async () => {
|
||||
await page.goto(customViewsUrl.list)
|
||||
await expect(
|
||||
page.locator('.view-description', {
|
||||
hasText: exactText('This is a custom view description component.'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
describe('filtering', () => {
|
||||
test('should prefill search input from query param', async () => {
|
||||
await createPost({ title: 'dennis' })
|
||||
@@ -764,15 +787,8 @@ describe('admin2', () => {
|
||||
describe('i18n', () => {
|
||||
test('should display translated collections and globals config options', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
// collection label
|
||||
await expect(page.locator('#nav-posts')).toContainText('Posts')
|
||||
|
||||
// global label
|
||||
await expect(page.locator('#nav-global-global')).toContainText('Global')
|
||||
|
||||
// view description
|
||||
await expect(page.locator('.view-description')).toContainText('Description')
|
||||
})
|
||||
|
||||
test('should display translated field titles', async () => {
|
||||
|
||||
Reference in New Issue
Block a user