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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -6,6 +6,11 @@ export const Global: GlobalConfig = {
|
||||
slug: globalSlug,
|
||||
admin: {
|
||||
components: {
|
||||
elements: {
|
||||
beforeDocumentControls: [
|
||||
'/components/BeforeDocumentControls/CustomDraftButton/index.js#CustomDraftButton',
|
||||
],
|
||||
},
|
||||
views: {
|
||||
edit: {
|
||||
api: {
|
||||
|
||||
Reference in New Issue
Block a user