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".