feat: adds beforeDocumentControls slot to allow custom component injection next to document controls (#12104)

### What

This PR introduces a new `beforeDocumentControls` slot to the edit view
of both collections and globals.

It allows injecting one or more custom components next to the document
control buttons (e.g., Save, Publish, Save Draft) in the admin UI —
useful for adding context, additional buttons, or custom UI elements.

#### Usage

##### For collections: 

```
admin: {
  components: {
    edit: {
      beforeDocumentControls: ['/path/to/CustomComponent'],
    },
  },
},
```

##### For globals:

```
admin: {
  components: {
    elements: {
      beforeDocumentControls: ['/path/to/CustomComponent'],
    },
  },
},
```
This commit is contained in:
Patrik
2025-04-17 15:23:17 -04:00
committed by GitHub
parent 34ea6ec14f
commit d55306980e
14 changed files with 216 additions and 21 deletions

View File

@@ -69,6 +69,12 @@ export const Posts: CollectionConfig = {
},
},
],
edit: {
beforeDocumentControls: [
'/components/BeforeDocumentControls/CustomDraftButton/index.js#CustomDraftButton',
'/components/BeforeDocumentControls/CustomSaveButton/index.js#CustomSaveButton',
],
},
},
pagination: {
defaultLimit: 5,

View File

@@ -0,0 +1,23 @@
import type { BeforeDocumentControlsServerProps } from 'payload'
import React from 'react'
const baseClass = 'custom-draft-button'
export function CustomDraftButton(props: BeforeDocumentControlsServerProps) {
return (
<div
className={baseClass}
id="custom-draft-button"
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Custom Draft Button
</p>
</div>
)
}

View File

@@ -0,0 +1,23 @@
import type { BeforeDocumentControlsServerProps } from 'payload'
import React from 'react'
const baseClass = 'custom-save-button'
export function CustomSaveButton(props: BeforeDocumentControlsServerProps) {
return (
<div
className={baseClass}
id="custom-save-button"
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<p className="nav__label" style={{ color: 'var(--theme-text)', margin: 0 }}>
Custom Save Button
</p>
</div>
)
}

View File

@@ -110,7 +110,7 @@ describe('Document View', () => {
})
expect(collectionItems.docs.length).toBe(1)
await page.goto(
`${postsUrl.collection(noApiViewGlobalSlug)}/${collectionItems.docs[0].id}/api`,
`${postsUrl.collection(noApiViewGlobalSlug)}/${collectionItems?.docs[0]?.id}/api`,
)
await expect(page.locator('.not-found')).toHaveCount(1)
})
@@ -333,20 +333,32 @@ describe('Document View', () => {
await navigateToDoc(page, postsUrl)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
await wait(500)
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
await expect(drawer1Content).toBeVisible()
const drawerLeft = await drawer1Content.boundingBox().then((box) => box.x)
const drawer1Box = await drawer1Content.boundingBox()
await expect.poll(() => drawer1Box).not.toBeNull()
const drawerLeft = drawer1Box!.x
await drawer1Content
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
const drawer2Content = page.locator('[id^=doc-drawer_posts_2_] .drawer__content')
await expect(drawer2Content).toBeVisible()
const drawer2Left = await drawer2Content.boundingBox().then((box) => box.x)
expect(drawer2Left > drawerLeft).toBe(true)
const drawer2Box = await drawer2Content.boundingBox()
await expect.poll(() => drawer2Box).not.toBeNull()
const drawer2Left = drawer2Box!.x
await expect.poll(() => drawer2Left > drawerLeft).toBe(true)
})
})
@@ -523,6 +535,24 @@ describe('Document View', () => {
await expect(fileField).toHaveValue('some file text')
})
})
describe('custom document controls', () => {
test('should show custom elements in document controls in collection', async () => {
await page.goto(postsUrl.create)
const customDraftButton = page.locator('#custom-draft-button')
const customSaveButton = page.locator('#custom-save-button')
await expect(customDraftButton).toBeVisible()
await expect(customSaveButton).toBeVisible()
})
test('should show custom elements in document controls in global', async () => {
await page.goto(globalURL.global(globalSlug))
const customDraftButton = page.locator('#custom-draft-button')
await expect(customDraftButton).toBeVisible()
})
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {

View File

@@ -6,6 +6,11 @@ export const Global: GlobalConfig = {
slug: globalSlug,
admin: {
components: {
elements: {
beforeDocumentControls: [
'/components/BeforeDocumentControls/CustomDraftButton/index.js#CustomDraftButton',
],
},
views: {
edit: {
api: {