diff --git a/docs/admin/preview.mdx b/docs/admin/preview.mdx
index b52b49e131f..3abfd20bfce 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 || '',
})
@@ -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',
+ },
+ ],
+}
+```
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/payload/src/collections/endpoints/index.ts b/packages/payload/src/collections/endpoints/index.ts
index b522170b6a0..8cf6005cb02 100644
--- a/packages/payload/src/collections/endpoints/index.ts
+++ b/packages/payload/src/collections/endpoints/index.ts
@@ -12,7 +12,6 @@ import { findByIDHandler } from './findByID.js'
// import { findDistinctHandler } from './findDistinct.js'
import { findVersionByIDHandler } from './findVersionByID.js'
import { findVersionsHandler } from './findVersions.js'
-import { previewHandler } from './preview.js'
import { restoreVersionHandler } from './restoreVersion.js'
import { updateHandler } from './update.js'
import { updateByIDHandler } from './updateByID.js'
@@ -75,11 +74,6 @@ export const defaultCollectionEndpoints: Endpoint[] = [
method: 'get',
path: '/versions/:id',
},
- {
- handler: previewHandler,
- method: 'get',
- path: '/:id/preview',
- },
{
handler: restoreVersionHandler,
method: 'post',
diff --git a/packages/payload/src/collections/endpoints/preview.ts b/packages/payload/src/collections/endpoints/preview.ts
deleted file mode 100644
index 758c34b03f6..00000000000
--- a/packages/payload/src/collections/endpoints/preview.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { status as httpStatus } from 'http-status'
-
-import type { PayloadHandler } from '../../config/types.js'
-
-import { extractJWT } from '../../auth/extractJWT.js'
-import { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js'
-import { headersWithCors } from '../../utilities/headersWithCors.js'
-import { isNumber } from '../../utilities/isNumber.js'
-import { findByIDOperation } from '../operations/findByID.js'
-
-export const previewHandler: PayloadHandler = async (req) => {
- const { id, collection } = getRequestCollectionWithID(req)
- const { searchParams } = req
- const depth = searchParams.get('depth')
-
- const doc = await findByIDOperation({
- id,
- collection,
- depth: isNumber(depth) ? Number(depth) : undefined,
- draft: searchParams.get('draft') === 'true',
- req,
- trash: true,
- })
-
- let previewURL!: string
-
- const generatePreviewURL =
- req.payload?.collections?.[collection.config.slug]?.config?.admin?.preview
-
- const token = extractJWT(req)
-
- if (typeof generatePreviewURL === 'function') {
- previewURL = (await generatePreviewURL(doc, {
- locale: req.locale!,
- req,
- token,
- })) as string
- }
-
- return Response.json(previewURL, {
- headers: headersWithCors({
- headers: new Headers(),
- req,
- }),
- status: httpStatus.OK,
- })
-}
diff --git a/packages/payload/src/globals/endpoints/index.ts b/packages/payload/src/globals/endpoints/index.ts
index a4b7b9293d7..55341c583bf 100644
--- a/packages/payload/src/globals/endpoints/index.ts
+++ b/packages/payload/src/globals/endpoints/index.ts
@@ -5,7 +5,6 @@ import { docAccessHandler } from './docAccess.js'
import { findOneHandler } from './findOne.js'
import { findVersionByIDHandler } from './findVersionByID.js'
import { findVersionsHandler } from './findVersions.js'
-import { previewHandler } from './preview.js'
import { restoreVersionHandler } from './restoreVersion.js'
import { updateHandler } from './update.js'
@@ -30,11 +29,6 @@ export const defaultGlobalEndpoints: Endpoint[] = wrapInternalEndpoints([
method: 'get',
path: '/versions',
},
- {
- handler: previewHandler,
- method: 'get',
- path: '/preview',
- },
{
handler: restoreVersionHandler,
method: 'post',
diff --git a/packages/payload/src/globals/endpoints/preview.ts b/packages/payload/src/globals/endpoints/preview.ts
deleted file mode 100644
index f5d88b1abf2..00000000000
--- a/packages/payload/src/globals/endpoints/preview.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { status as httpStatus } from 'http-status'
-
-import type { PayloadHandler } from '../../config/types.js'
-
-import { extractJWT } from '../../auth/extractJWT.js'
-import { getRequestGlobal } from '../../utilities/getRequestEntity.js'
-import { headersWithCors } from '../../utilities/headersWithCors.js'
-import { isNumber } from '../../utilities/isNumber.js'
-import { findOneOperation } from '../operations/findOne.js'
-
-export const previewHandler: PayloadHandler = async (req) => {
- const globalConfig = getRequestGlobal(req)
- const { searchParams } = req
- const depth = searchParams.get('depth')
-
- const doc = await findOneOperation({
- slug: globalConfig.slug,
- depth: isNumber(depth) ? Number(depth) : undefined,
- draft: searchParams.get('draft') === 'true',
- globalConfig,
- req,
- })
-
- let previewURL!: string
-
- const generatePreviewURL = req.payload.config.globals.find(
- (config) => config.slug === globalConfig.slug,
- )?.admin?.preview
-
- const token = extractJWT(req)
-
- if (typeof generatePreviewURL === 'function') {
- previewURL = (await generatePreviewURL(doc, {
- locale: req.locale!,
- req,
- token,
- }))!
- }
-
- return Response.json(previewURL, {
- headers: headersWithCors({
- headers: new Headers(),
- req,
- }),
- status: httpStatus.OK,
- })
-}
diff --git a/packages/ui/src/elements/PreviewButton/index.tsx b/packages/ui/src/elements/PreviewButton/index.tsx
index f1f0f2b5b06..ab1c22d6d8d 100644
--- a/packages/ui/src/elements/PreviewButton/index.tsx
+++ b/packages/ui/src/elements/PreviewButton/index.tsx
@@ -4,28 +4,30 @@ 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 (
-
+
)
}
diff --git a/packages/ui/src/elements/PreviewButton/usePreviewURL.tsx b/packages/ui/src/elements/PreviewButton/usePreviewURL.tsx
deleted file mode 100644
index 431cc9ff9a8..00000000000
--- a/packages/ui/src/elements/PreviewButton/usePreviewURL.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-'use client'
-import * as qs from 'qs-esm'
-import { useCallback, useRef, useState } from 'react'
-import { toast } from 'sonner'
-
-import { useConfig } from '../../providers/Config/index.js'
-import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
-import { useLocale } from '../../providers/Locale/index.js'
-import { useTranslation } from '../../providers/Translation/index.js'
-
-export const usePreviewURL = (): {
- generatePreviewURL: ({ openPreviewWindow }: { openPreviewWindow?: boolean }) => void
- isLoading: boolean
- label: string
- previewURL: string
-} => {
- const { id, collectionSlug, globalSlug, versionCount } = useDocumentInfo()
-
- const [isLoading, setIsLoading] = useState(false)
- const [previewURL, setPreviewURL] = useState('')
- const { code: locale } = useLocale()
-
- const hasVersions = versionCount > 0
-
- const {
- config: {
- routes: { api },
- serverURL,
- },
- } = useConfig()
-
- const { t } = useTranslation()
-
- const isGeneratingPreviewURL = useRef(false)
-
- // we need to regenerate the preview URL every time the button is clicked
- // to do this we need to fetch the document data fresh from the API
- // this will ensure the latest data is used when generating the preview URL
- const generatePreviewURL = useCallback(
- async ({ openPreviewWindow = false }) => {
- if (isGeneratingPreviewURL.current) {
- return
- }
-
- isGeneratingPreviewURL.current = true
-
- try {
- setIsLoading(true)
-
- let url = `${serverURL}${api}`
-
- if (collectionSlug) {
- url = `${url}/${collectionSlug}/${id}/preview`
- }
-
- if (globalSlug) {
- url = `${url}/globals/${globalSlug}/preview`
- }
-
- const params = {
- draft: hasVersions ? 'true' : 'false',
- locale: locale || undefined,
- }
-
- const res = await fetch(`${url}?${qs.stringify(params)}`)
-
- if (!res.ok) {
- throw new Error()
- }
-
- const newPreviewURL = await res.json()
-
- if (!newPreviewURL) {
- throw new Error()
- }
-
- setPreviewURL(newPreviewURL)
- setIsLoading(false)
- isGeneratingPreviewURL.current = false
-
- if (openPreviewWindow) {
- window.open(newPreviewURL, '_blank')
- }
- } catch (_err) {
- setIsLoading(false)
- isGeneratingPreviewURL.current = false
- toast.error(t('error:previewing'))
- }
- },
- [serverURL, api, collectionSlug, globalSlug, hasVersions, locale, id, t],
- )
-
- return {
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- generatePreviewURL,
- isLoading,
- label: isLoading ? t('general:loading') : t('version:preview'),
- previewURL,
- }
-}
diff --git a/packages/ui/src/exports/rsc/index.ts b/packages/ui/src/exports/rsc/index.ts
index 2e8d5221877..633f91a0d83 100644
--- a/packages/ui/src/exports/rsc/index.ts
+++ b/packages/ui/src/exports/rsc/index.ts
@@ -10,6 +10,7 @@ export { copyDataFromLocaleHandler } from '../../utilities/copyDataFromLocale.js
export { getColumns } from '../../utilities/getColumns.js'
export { getFolderResultsComponentAndData } from '../../utilities/getFolderResultsComponentAndData.js'
export { handleLivePreview } from '../../utilities/handleLivePreview.js'
+export { handlePreview } from '../../utilities/handlePreview.js'
export { renderFilters, renderTable } from '../../utilities/renderTable.js'
export { resolveFilterOptions } from '../../utilities/resolveFilterOptions.js'
export { upsertPreferences } from '../../utilities/upsertPreferences.js'
diff --git a/packages/ui/src/providers/LivePreview/context.ts b/packages/ui/src/providers/LivePreview/context.ts
index e42aae240e1..f51cafd7315 100644
--- a/packages/ui/src/providers/LivePreview/context.ts
+++ b/packages/ui/src/providers/LivePreview/context.ts
@@ -16,6 +16,7 @@ export interface LivePreviewContextType {
isLivePreviewEnabled: boolean
isLivePreviewing: boolean
isPopupOpen: boolean
+ isPreviewEnabled: boolean
listeningForMessages?: boolean
/**
* The URL that has finished loading in the iframe or popup.
@@ -29,6 +30,7 @@ export interface LivePreviewContextType {
}
openPopupWindow: ReturnType['openPopupWindow']
popupRef?: React.RefObject
+ previewURL?: string
previewWindowType: 'iframe' | 'popup'
setAppIsReady: (appIsReady: boolean) => void
setBreakpoint: (breakpoint: LivePreviewConfig['breakpoints'][number]['name']) => void
@@ -36,9 +38,11 @@ export interface LivePreviewContextType {
setIsLivePreviewing: (isLivePreviewing: boolean) => void
setLoadedURL: (loadedURL: string) => void
setMeasuredDeviceSize: (size: { height: number; width: number }) => void
+ setPreviewURL: (url: string) => void
setPreviewWindowType: (previewWindowType: 'iframe' | 'popup') => void
setSize: Dispatch
setToolbarPosition: (position: { x: number; y: number }) => void
+
/**
* Sets the URL of the preview (either iframe or popup).
* Will trigger a reload of the window.
@@ -71,12 +75,14 @@ export const LivePreviewContext = createContext({
isLivePreviewEnabled: undefined,
isLivePreviewing: false,
isPopupOpen: false,
+ isPreviewEnabled: undefined,
measuredDeviceSize: {
height: 0,
width: 0,
},
openPopupWindow: () => {},
popupRef: undefined,
+ previewURL: undefined,
previewWindowType: 'iframe',
setAppIsReady: () => {},
setBreakpoint: () => {},
@@ -84,6 +90,7 @@ export const LivePreviewContext = createContext({
setIsLivePreviewing: () => {},
setLoadedURL: () => {},
setMeasuredDeviceSize: () => {},
+ setPreviewURL: () => {},
setPreviewWindowType: () => {},
setSize: () => {},
setToolbarPosition: () => {},
@@ -104,3 +111,12 @@ export const LivePreviewContext = createContext({
})
export const useLivePreviewContext = () => use(LivePreviewContext)
+
+/**
+ * Hook to access live preview context values. Separated to prevent breaking changes. In the future this hook can be removed in favour of just using the LivePreview one.
+ */
+export const usePreviewURL = () => {
+ const { isPreviewEnabled, previewURL, setPreviewURL } = use(LivePreviewContext)
+
+ return { isPreviewEnabled, previewURL, setPreviewURL }
+}
diff --git a/packages/ui/src/providers/LivePreview/index.tsx b/packages/ui/src/providers/LivePreview/index.tsx
index 27e72b2d571..b0435ff9880 100644
--- a/packages/ui/src/providers/LivePreview/index.tsx
+++ b/packages/ui/src/providers/LivePreview/index.tsx
@@ -24,6 +24,14 @@ export type LivePreviewProviderProps = {
}
isLivePreviewEnabled?: boolean
isLivePreviewing: boolean
+ /**
+ * This specifically relates to `admin.preview` function in the config instead of live preview.
+ */
+ isPreviewEnabled?: boolean
+ /**
+ * This specifically relates to `admin.preview` function in the config instead of live preview.
+ */
+ previewURL?: string
} & Pick
export const LivePreviewProvider: React.FC = ({
@@ -31,6 +39,8 @@ export const LivePreviewProvider: React.FC = ({
children,
isLivePreviewEnabled,
isLivePreviewing: incomingIsLivePreviewing,
+ isPreviewEnabled,
+ previewURL: previewURLFromProps,
typeofLivePreviewURL,
url: urlFromProps,
}) => {
@@ -51,6 +61,7 @@ export const LivePreviewProvider: React.FC = ({
)
const [url, setURL] = useState('')
+ const [previewURL, setPreviewURL] = useState(previewURLFromProps)
const { isPopupOpen, openPopupWindow, popupRef } = usePopupWindow({
eventType: 'payload-live-preview',
@@ -247,11 +258,13 @@ export const LivePreviewProvider: React.FC = ({
isLivePreviewEnabled,
isLivePreviewing,
isPopupOpen,
+ isPreviewEnabled,
listeningForMessages,
loadedURL,
measuredDeviceSize,
openPopupWindow,
popupRef,
+ previewURL,
previewWindowType,
setAppIsReady,
setBreakpoint,
@@ -259,6 +272,7 @@ export const LivePreviewProvider: React.FC = ({
setIsLivePreviewing,
setLoadedURL,
setMeasuredDeviceSize,
+ setPreviewURL,
setPreviewWindowType: handleWindowChange,
setSize,
setToolbarPosition: setPosition,
diff --git a/packages/ui/src/utilities/buildFormState.ts b/packages/ui/src/utilities/buildFormState.ts
index 5ba409adce9..ccba19c4f74 100644
--- a/packages/ui/src/utilities/buildFormState.ts
+++ b/packages/ui/src/utilities/buildFormState.ts
@@ -17,6 +17,7 @@ import { getClientSchemaMap } from './getClientSchemaMap.js'
import { getSchemaMap } from './getSchemaMap.js'
import { handleFormStateLocking } from './handleFormStateLocking.js'
import { handleLivePreview } from './handleLivePreview.js'
+import { handlePreview } from './handlePreview.js'
export type LockedState = {
isLocked: boolean
@@ -30,12 +31,14 @@ type BuildFormStateSuccessResult = {
indexPath?: string
livePreviewURL?: string
lockedState?: LockedState
+ previewURL?: string
state: FormState
}
type BuildFormStateErrorResult = {
livePreviewURL?: never
lockedState?: never
+ previewURL?: never
state?: never
} & (
| {
@@ -100,6 +103,7 @@ export const buildFormState = async (
},
returnLivePreviewURL,
returnLockStatus,
+ returnPreviewURL,
schemaPath = collectionSlug || globalSlug,
select,
skipClientConfigAuth,
@@ -254,5 +258,21 @@ export const buildFormState = async (
}
}
+ if (returnPreviewURL) {
+ const { previewURL } = await handlePreview({
+ collectionSlug,
+ config,
+ data,
+ globalSlug,
+ req,
+ })
+
+ // Important: only set this when not undefined,
+ // Otherwise it will travel through the network as `$undefined`
+ if (previewURL) {
+ res.previewURL = previewURL
+ }
+ }
+
return res
}
diff --git a/packages/ui/src/utilities/handlePreview.ts b/packages/ui/src/utilities/handlePreview.ts
new file mode 100644
index 00000000000..e42bf050112
--- /dev/null
+++ b/packages/ui/src/utilities/handlePreview.ts
@@ -0,0 +1,95 @@
+import {
+ type CollectionConfig,
+ extractJWT,
+ type GlobalConfig,
+ type Operation,
+ type PayloadRequest,
+ type SanitizedConfig,
+} from 'payload'
+
+/**
+ * Multi-level check to determine whether live preview is enabled on a collection or global.
+ * For example, live preview can be enabled at both the root config level, or on the entity's config.
+ * If a collectionConfig/globalConfig is provided, checks if it is enabled at the root level,
+ * or on the entity's own config.
+ */
+export const isPreviewEnabled = ({
+ collectionConfig,
+ globalConfig,
+}: {
+ collectionConfig?: CollectionConfig
+ globalConfig?: GlobalConfig
+}): boolean => {
+ if (globalConfig) {
+ return Boolean(globalConfig.admin?.preview)
+ }
+
+ if (collectionConfig) {
+ return Boolean(collectionConfig.admin?.preview)
+ }
+}
+
+/**
+ * 1. Looks up the relevant live preview config, which could have been enabled:
+ * a. At the root level, e.g. `collections: ['posts']`
+ * b. On the collection or global config, e.g. `admin: { livePreview: { ... } }`
+ * 2. Determines if live preview is enabled, and if not, early returns.
+ * 3. Merges the config with the root config, if necessary.
+ * 4. Executes the `url` function, if necessary.
+ *
+ * Notice: internal function only. Subject to change at any time. Use at your own risk.
+ */
+export const handlePreview = async ({
+ collectionSlug,
+ config,
+ data,
+ globalSlug,
+ operation,
+ req,
+}: {
+ collectionSlug?: string
+ config: SanitizedConfig
+ data: Record
+ globalSlug?: string
+ operation?: Operation
+ req: PayloadRequest
+}): Promise<{
+ isPreviewEnabled?: boolean
+ previewURL?: string
+}> => {
+ const collectionConfig = collectionSlug
+ ? req.payload.collections[collectionSlug]?.config
+ : undefined
+
+ const globalConfig = globalSlug ? config.globals.find((g) => g.slug === globalSlug) : undefined
+
+ const enabled = isPreviewEnabled({
+ collectionConfig,
+ globalConfig,
+ })
+
+ if (!enabled) {
+ return {}
+ }
+
+ const generatePreviewURL = collectionConfig?.admin?.preview || globalConfig?.admin?.preview
+ const token = extractJWT(req)
+ let previewURL: string | undefined
+
+ if (typeof generatePreviewURL === 'function' && operation !== 'create') {
+ try {
+ const result = await generatePreviewURL(data, { locale: req.locale, req, token })
+
+ if (typeof result === 'string') {
+ previewURL = result
+ }
+ } catch (err) {
+ req.payload.logger.error({
+ err,
+ msg: `There was an error executing the live preview URL function for ${collectionSlug || globalSlug}`,
+ })
+ }
+ }
+
+ return { isPreviewEnabled: enabled, previewURL }
+}
diff --git a/packages/ui/src/views/Edit/index.tsx b/packages/ui/src/views/Edit/index.tsx
index 5aa162083ac..914ac029cc7 100644
--- a/packages/ui/src/views/Edit/index.tsx
+++ b/packages/ui/src/views/Edit/index.tsx
@@ -26,7 +26,7 @@ import { useConfig } from '../../providers/Config/index.js'
import { useDocumentEvents } from '../../providers/DocumentEvents/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useEditDepth } from '../../providers/EditDepth/index.js'
-import { useLivePreviewContext } from '../../providers/LivePreview/context.js'
+import { useLivePreviewContext, usePreviewURL } from '../../providers/LivePreview/context.js'
import { OperationProvider } from '../../providers/Operation/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
@@ -39,8 +39,8 @@ import { handleGoBack } from '../../utilities/handleGoBack.js'
import { handleTakeOver } from '../../utilities/handleTakeOver.js'
import { Auth } from './Auth/index.js'
import { SetDocumentStepNav } from './SetDocumentStepNav/index.js'
-import { SetDocumentTitle } from './SetDocumentTitle/index.js'
import './index.scss'
+import { SetDocumentTitle } from './SetDocumentTitle/index.js'
const baseClass = 'collection-edit'
@@ -146,6 +146,7 @@ export function DefaultEditView({
typeofLivePreviewURL,
url: livePreviewURL,
} = useLivePreviewContext()
+ const { isPreviewEnabled, setPreviewURL } = usePreviewURL()
const abortOnChangeRef = useRef(null)
const abortOnSaveRef = useRef(null)
@@ -338,7 +339,7 @@ export function DefaultEditView({
if (id || globalSlug) {
const docPreferences = await getDocPreferences()
- const { livePreviewURL, state } = await getFormState({
+ const { livePreviewURL, previewURL, state } = await getFormState({
id,
collectionSlug,
data: document,
@@ -350,6 +351,7 @@ export function DefaultEditView({
renderAllFields: false,
returnLivePreviewURL: isLivePreviewEnabled && typeofLivePreviewURL === 'function',
returnLockStatus: false,
+ returnPreviewURL: isPreviewEnabled,
schemaPath: schemaPathSegments.join('.'),
signal: controller.signal,
skipValidation: true,
@@ -364,6 +366,10 @@ export function DefaultEditView({
setLivePreviewURL(livePreviewURL)
}
+ if (isPreviewEnabled) {
+ setPreviewURL(previewURL)
+ }
+
reportUpdate({
id,
entitySlug,
@@ -389,6 +395,7 @@ export function DefaultEditView({
depth,
redirectAfterCreate,
setLivePreviewURL,
+ setPreviewURL,
globalSlug,
refreshCookieAsync,
incrementVersionCount,
@@ -403,6 +410,7 @@ export function DefaultEditView({
docPermissions,
operation,
isLivePreviewEnabled,
+ isPreviewEnabled,
typeofLivePreviewURL,
schemaPathSegments,
isLockingEnabled,
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()
})
})
diff --git a/test/live-preview/collections/ConditionalURL.ts b/test/live-preview/collections/ConditionalURL.ts
index c256df4b605..74b3d5ecd26 100644
--- a/test/live-preview/collections/ConditionalURL.ts
+++ b/test/live-preview/collections/ConditionalURL.ts
@@ -6,6 +6,9 @@ export const ConditionalURL: CollectionConfig = {
livePreview: {
url: ({ data }) => (data?.enabled ? '/live-preview/static' : null),
},
+ preview: (doc) => {
+ return doc?.enabled ? '/live-preview/static' : null
+ },
},
fields: [
{
diff --git a/test/live-preview/e2e.spec.ts b/test/live-preview/e2e.spec.ts
index 64b1a48a683..e2b43cd3a09 100644
--- a/test/live-preview/e2e.spec.ts
+++ b/test/live-preview/e2e.spec.ts
@@ -222,6 +222,32 @@ describe('Live Preview', () => {
await expect(page.locator('iframe.live-preview-iframe')).toBeHidden()
})
+ test('collection — does not render preview button when url is null', async () => {
+ const noURL = new AdminUrlUtil(serverURL, 'conditional-url')
+ await page.goto(noURL.create)
+ await page.locator('#field-title').fill('No URL')
+ await saveDocAndAssert(page)
+
+ // No button should render
+ const previewButton = page.locator('#preview-button')
+ await expect(previewButton).toBeHidden()
+
+ // Check the `enabled` field
+ const enabledCheckbox = page.locator('#field-enabled')
+ await enabledCheckbox.check()
+ await saveDocAndAssert(page)
+
+ // Button is present
+ await expect(previewButton).toBeVisible()
+
+ // Uncheck the `enabled` field
+ await enabledCheckbox.uncheck()
+ await saveDocAndAssert(page)
+
+ // Button is gone
+ await expect(previewButton).toBeHidden()
+ })
+
test('collection — retains static URL across edits', async () => {
const util = new AdminUrlUtil(serverURL, 'static-url')
await page.goto(util.create)
diff --git a/test/live-preview/payload-types.ts b/test/live-preview/payload-types.ts
index b89380fcbb2..840059afd2a 100644
--- a/test/live-preview/payload-types.ts
+++ b/test/live-preview/payload-types.ts
@@ -80,6 +80,7 @@ export interface Config {
'collection-level-config': CollectionLevelConfig;
'static-url': StaticUrl;
'custom-live-preview': CustomLivePreview;
+ 'conditional-url': ConditionalUrl;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -97,6 +98,7 @@ export interface Config {
'collection-level-config': CollectionLevelConfigSelect | CollectionLevelConfigSelect;
'static-url': StaticUrlSelect | StaticUrlSelect;
'custom-live-preview': CustomLivePreviewSelect | CustomLivePreviewSelect;
+ 'conditional-url': ConditionalUrlSelect | ConditionalUrlSelect;
'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect;
'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect;
'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect;
@@ -1042,6 +1044,17 @@ export interface CustomLivePreview {
updatedAt: string;
createdAt: string;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "conditional-url".
+ */
+export interface ConditionalUrl {
+ id: string;
+ title?: string | null;
+ enabled?: boolean | null;
+ updatedAt: string;
+ createdAt: string;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -1092,6 +1105,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'custom-live-preview';
value: string | CustomLivePreview;
+ } | null)
+ | ({
+ relationTo: 'conditional-url';
+ value: string | ConditionalUrl;
} | null);
globalSlug?: string | null;
user: {
@@ -1742,6 +1759,16 @@ export interface CustomLivePreviewSelect {
updatedAt?: T;
createdAt?: T;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "conditional-url_select".
+ */
+export interface ConditionalUrlSelect {
+ title?: T;
+ enabled?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".