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:
Jacob Fletcher
2024-08-21 10:20:22 -04:00
committed by GitHub
parent cad1906725
commit cb9b80aaf9
14 changed files with 59 additions and 37 deletions

View File

@@ -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. |

View File

@@ -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>

View File

@@ -85,6 +85,7 @@ export async function generateImportMap(
if (shouldLog) {
console.log('Generating import map')
}
const importMap: InternalImportMap = {}
const imports: Imports = {}

View File

@@ -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)

View File

@@ -24,7 +24,6 @@ export type ClientCollectionConfig = {
beforeList: MappedComponent[]
beforeListTable: MappedComponent[]
edit: {
Description: MappedComponent
PreviewButton: MappedComponent
PublishButton: MappedComponent
SaveButton: MappedComponent

View File

@@ -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
*/

View File

@@ -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 && (

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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: {

View 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>
}

View File

@@ -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()

View File

@@ -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 () => {