diff --git a/docs/custom-components/edit-view.mdx b/docs/custom-components/edit-view.mdx index 4f26004a86..03043e2daf 100644 --- a/docs/custom-components/edit-view.mdx +++ b/docs/custom-components/edit-view.mdx @@ -101,14 +101,15 @@ export const MyCollection: CollectionConfig = { The following options are available: -| Path | Description | -| ----------------- | -------------------------------------------------------------------------------------- | -| `SaveButton` | A button that saves the current document. [More details](#savebutton). | -| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). | -| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). | -| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). | -| `Description` | A description of the Collection. [More details](#description). | -| `Upload` | A file upload component. [More details](#upload). | +| Path | Description | +| ------------------------ | ---------------------------------------------------------------------------------------------------- | +| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). | +| `SaveButton` | A button that saves the current document. [More details](#savebutton). | +| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). | +| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). | +| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). | +| `Description` | A description of the Collection. [More details](#description). | +| `Upload` | A file upload component. [More details](#upload). | #### Globals @@ -133,13 +134,14 @@ export const MyGlobal: GlobalConfig = { The following options are available: -| Path | Description | -| ----------------- | -------------------------------------------------------------------------------------- | -| `SaveButton` | A button that saves the current document. [More details](#savebutton). | -| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). | -| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). | -| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). | -| `Description` | A description of the Global. [More details](#description). | +| Path | Description | +| ------------------------ | ---------------------------------------------------------------------------------------------------- | +| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). | +| `SaveButton` | A button that saves the current document. [More details](#savebutton). | +| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). | +| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). | +| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). | +| `Description` | A description of the Global. [More details](#description). | ### SaveButton @@ -191,6 +193,73 @@ export function MySaveButton(props: SaveButtonClientProps) { } ``` +### beforeDocumentControls + +The `beforeDocumentControls` property allows you to render custom components just before the default document action buttons (like Save, Publish, or Preview). This is useful for injecting custom buttons, status indicators, or any other UI elements before the built-in controls. + +To add `beforeDocumentControls` components, use the `components.edit.beforeDocumentControls` property in you [Collection Config](../configuration/collections) or `components.elements.beforeDocumentControls` in your [Global Config](../configuration/globals): + +#### Collections + +``` +export const MyCollection: CollectionConfig = { + admin: { + components: { + edit: { + // highlight-start + beforeDocumentControls: ['/path/to/CustomComponent'], + // highlight-end + }, + }, + }, +} +``` + +#### Globals + +``` +export const MyGlobal: GlobalConfig = { + admin: { + components: { + elements: { + // highlight-start + beforeDocumentControls: ['/path/to/CustomComponent'], + // highlight-end + }, + }, + }, +} +``` + +Here's an example of a custom `beforeDocumentControls` component: + +#### Server Component + +```tsx +import React from 'react' +import type { BeforeDocumentControlsServerProps } from 'payload' + +export function MyCustomDocumentControlButton( + props: BeforeDocumentControlsServerProps, +) { + return
This is a custom beforeDocumentControl button (Server)
+} +``` + +#### Client Component + +```tsx +'use client' +import React from 'react' +import type { BeforeDocumentControlsClientProps } from 'payload' + +export function MyCustomDocumentControlButton( + props: BeforeDocumentControlsClientProps, +) { + return
This is a custom beforeDocumentControl button (Client)
+} +``` + ### SaveDraftButton The `SaveDraftButton` property allows you to render a custom Save Draft Button in the Edit View. diff --git a/packages/next/src/views/Document/renderDocumentSlots.tsx b/packages/next/src/views/Document/renderDocumentSlots.tsx index dde74e5e26..dee93e9410 100644 --- a/packages/next/src/views/Document/renderDocumentSlots.tsx +++ b/packages/next/src/views/Document/renderDocumentSlots.tsx @@ -1,4 +1,5 @@ import type { + BeforeDocumentControlsServerPropsOnly, DefaultServerFunctionArgs, DocumentSlots, PayloadRequest, @@ -42,6 +43,18 @@ export const renderDocumentSlots: (args: { // TODO: Add remaining serverProps } + const BeforeDocumentControls = + collectionConfig?.admin?.components?.edit?.beforeDocumentControls || + globalConfig?.admin?.components?.elements?.beforeDocumentControls + + if (BeforeDocumentControls) { + components.BeforeDocumentControls = RenderServerComponent({ + Component: BeforeDocumentControls, + importMap: req.payload.importMap, + serverProps: serverProps satisfies BeforeDocumentControlsServerPropsOnly, + }) + } + const CustomPreviewButton = collectionConfig?.admin?.components?.edit?.PreviewButton || globalConfig?.admin?.components?.elements?.PreviewButton diff --git a/packages/payload/src/admin/types.ts b/packages/payload/src/admin/types.ts index f98d8075b4..40fb5abf40 100644 --- a/packages/payload/src/admin/types.ts +++ b/packages/payload/src/admin/types.ts @@ -553,6 +553,7 @@ export type FieldRow = { } export type DocumentSlots = { + BeforeDocumentControls?: React.ReactNode Description?: React.ReactNode PreviewButton?: React.ReactNode PublishButton?: React.ReactNode @@ -578,6 +579,9 @@ export type { LanguageOptions } from './LanguageOptions.js' export type { RichTextAdapter, RichTextAdapterProvider, RichTextHooks } from './RichText.js' export type { + BeforeDocumentControlsClientProps, + BeforeDocumentControlsServerProps, + BeforeDocumentControlsServerPropsOnly, DocumentSubViewTypes, DocumentTabClientProps, /** diff --git a/packages/payload/src/admin/views/document.ts b/packages/payload/src/admin/views/document.ts index db0b2ee2db..7e8d14f145 100644 --- a/packages/payload/src/admin/views/document.ts +++ b/packages/payload/src/admin/views/document.ts @@ -36,12 +36,12 @@ export type DocumentTabServerPropsOnly = { readonly permissions: SanitizedPermissions } & ServerProps -export type DocumentTabServerProps = DocumentTabClientProps & DocumentTabServerPropsOnly - export type DocumentTabClientProps = { path: string } +export type DocumentTabServerProps = DocumentTabClientProps & DocumentTabServerPropsOnly + export type DocumentTabCondition = (args: { collectionConfig: SanitizedCollectionConfig config: SanitizedConfig @@ -75,3 +75,10 @@ export type DocumentTabConfig = { export type DocumentTabComponent = PayloadComponent<{ path: string }> + +// BeforeDocumentControls + +export type BeforeDocumentControlsClientProps = {} +export type BeforeDocumentControlsServerPropsOnly = {} & ServerProps +export type BeforeDocumentControlsServerProps = BeforeDocumentControlsClientProps & + BeforeDocumentControlsServerPropsOnly diff --git a/packages/payload/src/bin/generateImportMap/iterateCollections.ts b/packages/payload/src/bin/generateImportMap/iterateCollections.ts index 9aa7783576..27134fc86f 100644 --- a/packages/payload/src/bin/generateImportMap/iterateCollections.ts +++ b/packages/payload/src/bin/generateImportMap/iterateCollections.ts @@ -36,6 +36,7 @@ export function iterateCollections({ addToImportMap(collection.admin?.components?.beforeListTable) addToImportMap(collection.admin?.components?.Description) + addToImportMap(collection.admin?.components?.edit?.beforeDocumentControls) addToImportMap(collection.admin?.components?.edit?.PreviewButton) addToImportMap(collection.admin?.components?.edit?.PublishButton) addToImportMap(collection.admin?.components?.edit?.SaveButton) diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 7478a64af6..90be14abdc 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -279,6 +279,10 @@ export type CollectionAdminOptions = { * Components within the edit view */ edit?: { + /** + * Inject custom components before the document controls + */ + beforeDocumentControls?: CustomComponent[] /** * Replaces the "Preview" button */ diff --git a/packages/payload/src/globals/config/types.ts b/packages/payload/src/globals/config/types.ts index 7521695068..a2bbeb94c1 100644 --- a/packages/payload/src/globals/config/types.ts +++ b/packages/payload/src/globals/config/types.ts @@ -9,6 +9,7 @@ import type { } from '../../admin/types.js' import type { Access, + CustomComponent, EditConfig, Endpoint, EntityDescription, @@ -80,6 +81,10 @@ export type GlobalAdminOptions = { */ components?: { elements?: { + /** + * Inject custom components before the document controls + */ + beforeDocumentControls?: CustomComponent[] Description?: EntityDescriptionComponent /** * Replaces the "Preview" button diff --git a/packages/ui/src/elements/DocumentControls/index.tsx b/packages/ui/src/elements/DocumentControls/index.tsx index 2c0a6ed3e5..f13be0fd15 100644 --- a/packages/ui/src/elements/DocumentControls/index.tsx +++ b/packages/ui/src/elements/DocumentControls/index.tsx @@ -37,6 +37,7 @@ const baseClass = 'doc-controls' export const DocumentControls: React.FC<{ readonly apiURL: string + readonly BeforeDocumentControls?: React.ReactNode readonly customComponents?: { readonly PreviewButton?: React.ReactNode readonly PublishButton?: React.ReactNode @@ -67,6 +68,7 @@ export const DocumentControls: React.FC<{ const { id, slug, + BeforeDocumentControls, customComponents: { PreviewButton: CustomPreviewButton, PublishButton: CustomPublishButton, @@ -222,6 +224,7 @@ export const DocumentControls: React.FC<{
+ {BeforeDocumentControls} {(collectionConfig?.admin.preview || globalConfig?.admin.preview) && ( +

+ Custom Draft Button +

+
+ ) +} diff --git a/test/admin/components/BeforeDocumentControls/CustomSaveButton/index.tsx b/test/admin/components/BeforeDocumentControls/CustomSaveButton/index.tsx new file mode 100644 index 0000000000..d4ef70125e --- /dev/null +++ b/test/admin/components/BeforeDocumentControls/CustomSaveButton/index.tsx @@ -0,0 +1,23 @@ +import type { BeforeDocumentControlsServerProps } from 'payload' + +import React from 'react' + +const baseClass = 'custom-save-button' + +export function CustomSaveButton(props: BeforeDocumentControlsServerProps) { + return ( +
+

+ Custom Save Button +

+
+ ) +} diff --git a/test/admin/e2e/document-view/e2e.spec.ts b/test/admin/e2e/document-view/e2e.spec.ts index 0fee868040..d3830b255d 100644 --- a/test/admin/e2e/document-view/e2e.spec.ts +++ b/test/admin/e2e/document-view/e2e.spec.ts @@ -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): Promise { diff --git a/test/admin/globals/Global.ts b/test/admin/globals/Global.ts index e654c584b6..7f0d615102 100644 --- a/test/admin/globals/Global.ts +++ b/test/admin/globals/Global.ts @@ -6,6 +6,11 @@ export const Global: GlobalConfig = { slug: globalSlug, admin: { components: { + elements: { + beforeDocumentControls: [ + '/components/BeforeDocumentControls/CustomDraftButton/index.js#CustomDraftButton', + ], + }, views: { edit: { api: {