From d572ac6eabe485bf0f2a67d2ace09c244eb52fa1 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Tue, 21 Oct 2025 10:36:07 +0100 Subject: [PATCH 1/9] fix(ui): preview button not responding to conditional URL by adding it into the form state --- packages/next/src/views/Document/index.tsx | 13 ++- packages/payload/src/admin/forms/Form.ts | 6 ++ .../ui/src/elements/PreviewButton/index.tsx | 18 ++-- .../elements/PreviewButton/usePreviewURL.tsx | 100 ------------------ packages/ui/src/exports/rsc/index.ts | 1 + .../ui/src/providers/LivePreview/context.ts | 16 +++ .../ui/src/providers/LivePreview/index.tsx | 14 +++ packages/ui/src/utilities/buildFormState.ts | 20 ++++ packages/ui/src/utilities/handlePreview.ts | 95 +++++++++++++++++ packages/ui/src/views/Edit/index.tsx | 14 ++- 10 files changed, 186 insertions(+), 111 deletions(-) delete mode 100644 packages/ui/src/elements/PreviewButton/usePreviewURL.tsx create mode 100644 packages/ui/src/utilities/handlePreview.ts diff --git a/packages/next/src/views/Document/index.tsx b/packages/next/src/views/Document/index.tsx index 0cd1d56e1ee..4b28aa699be 100644 --- a/packages/next/src/views/Document/index.tsx +++ b/packages/next/src/views/Document/index.tsx @@ -17,7 +17,7 @@ import { LivePreviewProvider, } from '@payloadcms/ui' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' -import { handleLivePreview } from '@payloadcms/ui/rsc' +import { handleLivePreview, handlePreview } from '@payloadcms/ui/rsc' import { isEditing as getIsEditing } from '@payloadcms/ui/shared' import { buildFormState } from '@payloadcms/ui/utilities/buildFormState' import { notFound, redirect } from 'next/navigation.js' @@ -360,6 +360,15 @@ export const renderDocument = async ({ req, }) + const { isPreviewEnabled, previewURL } = await handlePreview({ + collectionSlug, + config, + data: doc, + globalSlug, + operation, + req, + }) + return { data: doc, Document: ( @@ -395,6 +404,8 @@ export const renderDocument = async ({ isLivePreviewing={Boolean( entityPreferences?.value?.editViewType === 'live-preview' && livePreviewURL, )} + isPreviewEnabled={Boolean(isPreviewEnabled)} + previewURL={previewURL} typeofLivePreviewURL={typeof livePreviewConfig?.url as 'function' | 'string' | undefined} url={livePreviewURL} > diff --git a/packages/payload/src/admin/forms/Form.ts b/packages/payload/src/admin/forms/Form.ts index d02039204be..5e6e92b30ab 100644 --- a/packages/payload/src/admin/forms/Form.ts +++ b/packages/payload/src/admin/forms/Form.ts @@ -142,6 +142,12 @@ export type BuildFormStateArgs = { */ returnLivePreviewURL?: boolean returnLockStatus?: boolean + /** + * If true, will return a fresh URL for preview based on the current form state. + * Note: this will run on every form state event, so if your `preview` function is long running or expensive, + * ensure it caches itself as needed. + */ + returnPreviewURL?: boolean schemaPath: string select?: SelectType /** diff --git a/packages/ui/src/elements/PreviewButton/index.tsx b/packages/ui/src/elements/PreviewButton/index.tsx index f1f0f2b5b06..f757023c6ca 100644 --- a/packages/ui/src/elements/PreviewButton/index.tsx +++ b/packages/ui/src/elements/PreviewButton/index.tsx @@ -4,25 +4,29 @@ import type { PreviewButtonClientProps } from 'payload' import React from 'react' import { ExternalLinkIcon } from '../../icons/ExternalLink/index.js' -import { usePreviewURL } from './usePreviewURL.js' import './index.scss' +import { usePreviewURL } from '../../providers/LivePreview/context.js' +import { useTranslation } from '../../providers/Translation/index.js' const baseClass = 'preview-btn' export function PreviewButton(props: PreviewButtonClientProps) { - const { generatePreviewURL, label } = usePreviewURL() + const { previewURL } = usePreviewURL() + const { t } = useTranslation() + + if (!previewURL) { + return null + } return ( + ) } From 0cf18f5a9106d5e8030422b5fc5d780a97a7a9d4 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Tue, 21 Oct 2025 15:49:53 +0100 Subject: [PATCH 5/9] fix test --- test/live-preview/e2e.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/live-preview/e2e.spec.ts b/test/live-preview/e2e.spec.ts index ef7b684782d..e2b43cd3a09 100644 --- a/test/live-preview/e2e.spec.ts +++ b/test/live-preview/e2e.spec.ts @@ -229,7 +229,7 @@ describe('Live Preview', () => { await saveDocAndAssert(page) // No button should render - const previewButton = page.locator('button#preview-button') + const previewButton = page.locator('#preview-button') await expect(previewButton).toBeHidden() // Check the `enabled` field From fa9055a4643fdcdcddaff6e4545e27043a1e4010 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Tue, 21 Oct 2025 15:50:22 +0100 Subject: [PATCH 6/9] fix some docs info --- docs/admin/preview.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/admin/preview.mdx b/docs/admin/preview.mdx index b52b49e131f..5afa3ec567a 100644 --- a/docs/admin/preview.mdx +++ b/docs/admin/preview.mdx @@ -81,10 +81,10 @@ import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', admin: { - preview: ({ slug, collection }) => { + preview: ({ slug }) => { const encodedParams = new URLSearchParams({ slug, - collection, + collection: 'pages', path: `/${slug}`, previewSecret: process.env.PREVIEW_SECRET || '', }) From ddc81dce1b8c6540a569c6cebc9c5291c1bfd994 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Tue, 21 Oct 2025 15:57:47 +0100 Subject: [PATCH 7/9] add docs section on conditional preview urls --- docs/admin/preview.mdx | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/admin/preview.mdx b/docs/admin/preview.mdx index 5afa3ec567a..3abfd20bfce 100644 --- a/docs/admin/preview.mdx +++ b/docs/admin/preview.mdx @@ -231,3 +231,32 @@ export default async function Page({ params: paramsPromise }) { in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples). + +### Conditional Preview URLs + +You can also conditionally enable or disable the preview button based on the document's data. This is useful for scenarios where you only want to show the preview button when certain criteria are met. + +To do this, simply return `null` from the `preview` function when you want to hide the preview button: + +```ts +import type { CollectionConfig } from 'payload' + +export const Pages: CollectionConfig = { + slug: 'pages', + admin: { + preview: (doc) => { + return doc?.enabled ? `http://localhost:3000/${doc.slug}` : null + }, + }, + fields: [ + { + name: 'slug', + type: 'text', + }, + { + name: 'enabled', + type: 'checkbox', + }, + ], +} +``` From 50dd0d5e7d088f25e4cfeb3482b41adc139fc44f Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Wed, 22 Oct 2025 17:41:29 +0100 Subject: [PATCH 8/9] retrigger ci From 1543c80af6cffe2d7c65e382a06ecbab6b0dbaf8 Mon Sep 17 00:00:00 2001 From: Paul Popus Date: Wed, 22 Oct 2025 20:49:55 +0100 Subject: [PATCH 9/9] fix e2e --- test/admin/e2e/document-view/e2e.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/admin/e2e/document-view/e2e.spec.ts b/test/admin/e2e/document-view/e2e.spec.ts index c2c2747f3c1..249899f1e57 100644 --- a/test/admin/e2e/document-view/e2e.spec.ts +++ b/test/admin/e2e/document-view/e2e.spec.ts @@ -160,7 +160,7 @@ describe('Document View', () => { await page.goto(collectionWithPreview.create) await page.locator('#field-title').fill(title) await saveDocAndAssert(page) - await expect(page.locator('button#preview-button')).toBeVisible() + await expect(page.locator('#preview-button')).toBeVisible() }) test('collection — should not render preview button when `admin.preview` is not set', async () => { @@ -168,13 +168,13 @@ describe('Document View', () => { await page.goto(collectionWithoutPreview.create) await page.locator('#field-title').fill(title) await saveDocAndAssert(page) - await expect(page.locator('button#preview-button')).toBeHidden() + await expect(page.locator('#preview-button')).toBeHidden() }) test('global — should render preview button when `admin.preview` is set', async () => { const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug) await page.goto(globalWithPreview.global(globalSlug)) - await expect(page.locator('button#preview-button')).toBeVisible() + await expect(page.locator('#preview-button')).toBeVisible() }) test('global — should not render preview button when `admin.preview` is not set', async () => { @@ -182,7 +182,7 @@ describe('Document View', () => { await page.goto(globalWithoutPreview.global(group1GlobalSlug)) await page.locator('#field-title').fill(title) await saveDocAndAssert(page) - await expect(page.locator('button#preview-button')).toBeHidden() + await expect(page.locator('#preview-button')).toBeHidden() }) })