Skip to content

Commit 2be6bb3

Browse files
authored
feat(ui): live preview conditions (#14012)
Supports live preview conditions. This is essentially access control for live preview, where you may want to restrict who can use it based on certain criteria, such as the current user or document data. To do this, simply return null or undefined from your live preview url functions: ```ts url: ({ req }) => (req.user?.role === 'admin' ? '/hello-world' : null) ``` This is also useful for pages which derive their URL from document data, e.g. a slug field, do not attempt to render the live preview window until the URL is fully formatted. For example, if you have a page in your front-end with the URL structure of `/posts/[slug]`, the slug field is required before the page can properly load. However, if the slug is not a required field, or when drafts and/or autosave is enabled, the slug field might not yet have data, leading to `/posts/undefined` or similar. ```ts url: ({ data }) => data?.slug ? `/${data.slug}` : null ``` --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211513433305000
1 parent 537f58b commit 2be6bb3

File tree

14 files changed

+143
-18
lines changed

14 files changed

+143
-18
lines changed

docs/live-preview/overview.mdx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const config = buildConfig({
3939

4040
## Options
4141

42-
Setting up Live Preview is easy. This can be done either globally through the [Root Admin Config](../admin/overview), or on individual [Collection Admin Configs](../configuration/collections#admin-options) and [Global Admin Configs](../configuration/globals#admin-options). Once configured, a new "Live Preview" tab will appear at the top of enabled Documents. Navigating to this tab opens the preview window and loads your front-end application.
42+
Setting up Live Preview is easy. This can be done either globally through the [Root Admin Config](../admin/overview), or on individual [Collection Admin Configs](../configuration/collections#admin-options) and [Global Admin Configs](../configuration/globals#admin-options). Once configured, a new "Live Preview" button will appear at the top of enabled Documents. Toggling this button opens the preview window and loads your front-end application.
4343

4444
The following options are available:
4545

@@ -75,6 +75,8 @@ const config = buildConfig({
7575

7676
You can also pass a function in order to dynamically format URLs. This is useful for multi-tenant applications, localization, or any other scenario where the URL needs to be generated based on the Document being edited.
7777

78+
This is also useful for conditionally rendering Live Preview, similar to access control. See [Conditional Rendering](./conditional-rendering) for more details.
79+
7880
To set dynamic URLs, set the `admin.livePreview.url` property in your [Payload Config](../configuration/overview) to a function:
7981

8082
```ts
@@ -114,7 +116,17 @@ You can return either an absolute URL or relative URL from this function. If you
114116
If your application requires a fully qualified URL, or you are attempting to preview with a frontend on a different domain, you can use the `req` property to build this URL:
115117

116118
```ts
117-
url: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}` // highlight-line
119+
url: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}`
120+
```
121+
122+
#### Conditional Rendering
123+
124+
You can conditionally render Live Preview by returning `undefined` or `null` from the `url` function. This is similar to access control, where you may want to restrict who can use Live Preview based on certain criteria, such as the current user or document data.
125+
126+
For example, you could check the user's role and only enable Live Preview if they have the appropriate permissions:
127+
128+
```ts
129+
url: ({ req }) => (req.user?.role === 'admin' ? '/hello-world' : null)
118130
```
119131

120132
### Breakpoints

packages/payload/src/collections/config/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,9 @@ export type CollectionAdminOptions = {
444444
*/
445445
listSearchableFields?: string[]
446446
/**
447-
* Live preview options
447+
* Live Preview options.
448+
*
449+
* @see https://payloadcms.com/docs/live-preview/overview
448450
*/
449451
livePreview?: LivePreviewConfig
450452
meta?: MetaConfig

packages/payload/src/config/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { I18nClient } from '@payloadcms/translations'
22
import type { DeepPartial } from 'ts-essentials'
33

44
import type { ImportMap } from '../bin/generateImportMap/index.js'
5-
import type { ClientBlock, ClientField, Field } from '../fields/config/types.js'
5+
import type { ClientBlock } from '../fields/config/types.js'
66
import type { BlockSlug, TypedUser } from '../index.js'
77
import type {
88
RootLivePreviewConfig,

packages/payload/src/config/types.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ type Prettify<T> = {
138138

139139
export type Plugin = (config: Config) => Config | Promise<Config>
140140

141+
export type LivePreviewURLType = null | string | undefined
142+
141143
export type LivePreviewConfig = {
142144
/**
143145
Device breakpoints to use for the `iframe` of the Live Preview window.
@@ -154,7 +156,9 @@ export type LivePreviewConfig = {
154156
* The URL of the frontend application. This will be rendered within an `iframe` as its `src`.
155157
* Payload will send a `window.postMessage()` to this URL with the document data in real-time.
156158
* The frontend application is responsible for receiving the message and updating the UI accordingly.
157-
* Use the `useLivePreview` hook to get started in React applications.
159+
* @see https://payloadcms.com/docs/live-preview/frontend
160+
*
161+
* To conditionally render Live Preview, use a function that returns `undefined` or `null`.
158162
*
159163
* Note: this function may run often if autosave is enabled with a small interval.
160164
* For performance, avoid long-running tasks or expensive operations within this function,
@@ -172,8 +176,8 @@ export type LivePreviewConfig = {
172176
*/
173177
payload: Payload
174178
req: PayloadRequest
175-
}) => Promise<string> | string)
176-
| string
179+
}) => LivePreviewURLType | Promise<LivePreviewURLType>)
180+
| LivePreviewURLType
177181
}
178182

179183
export type RootLivePreviewConfig = {
@@ -884,6 +888,11 @@ export type Config = {
884888
*/
885889
importMapFile?: string
886890
}
891+
/**
892+
* Live Preview options.
893+
*
894+
* @see https://payloadcms.com/docs/live-preview/overview
895+
*/
887896
livePreview?: RootLivePreviewConfig
888897
/** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */
889898
meta?: MetaConfig

packages/ui/src/elements/LivePreview/Toggler/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ import './index.scss'
88
const baseClass = 'live-preview-toggler'
99

1010
export const LivePreviewToggler: React.FC = () => {
11-
const { isLivePreviewing, setIsLivePreviewing } = useLivePreviewContext()
11+
const { isLivePreviewing, setIsLivePreviewing, url: livePreviewURL } = useLivePreviewContext()
1212
const { t } = useTranslation()
1313

14+
if (!livePreviewURL) {
15+
return null
16+
}
17+
1418
return (
1519
<button
1620
aria-label={isLivePreviewing ? t('general:exitLivePreview') : t('general:livePreview')}

packages/ui/src/providers/LivePreview/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { LivePreviewConfig } from 'payload'
2+
import type { LivePreviewConfig, LivePreviewURLType } from 'payload'
33
import type { Dispatch } from 'react'
44
import type React from 'react'
55

@@ -59,7 +59,7 @@ export interface LivePreviewContextType {
5959
* It is important to know which one it is, so that we can opt in/out of certain behaviors, e.g. calling the server to get the URL.
6060
*/
6161
typeofLivePreviewURL?: 'function' | 'string'
62-
url: string | undefined
62+
url: LivePreviewURLType
6363
zoom: number
6464
}
6565

packages/ui/src/providers/LivePreview/index.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { CollectionPreferences, LivePreviewConfig } from 'payload'
2+
import type { CollectionPreferences, LivePreviewConfig, LivePreviewURLType } from 'payload'
33

44
import { DndContext } from '@dnd-kit/core'
55
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -92,7 +92,11 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
9292
*/
9393
const setLivePreviewURL = useCallback<LivePreviewContextType['setURL']>(
9494
(_incomingURL) => {
95-
const incomingURL = formatAbsoluteURL(_incomingURL)
95+
let incomingURL: LivePreviewURLType
96+
97+
if (typeof _incomingURL === 'string') {
98+
incomingURL = formatAbsoluteURL(_incomingURL)
99+
}
96100

97101
if (incomingURL !== url) {
98102
setAppIsReady(false)
@@ -106,7 +110,9 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
106110
* `url` needs to be relative to the window, which cannot be done on initial render.
107111
*/
108112
useEffect(() => {
109-
setURL(formatAbsoluteURL(urlFromProps))
113+
if (typeof urlFromProps === 'string') {
114+
setURL(formatAbsoluteURL(urlFromProps))
115+
}
110116
}, [urlFromProps])
111117

112118
// The toolbar needs to freely drag and drop around the page

packages/ui/src/utilities/handleLivePreview.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
CollectionConfig,
33
GlobalConfig,
44
LivePreviewConfig,
5+
LivePreviewURLType,
56
Operation,
67
PayloadRequest,
78
SanitizedConfig,
@@ -80,7 +81,7 @@ export const handleLivePreview = async ({
8081
}): Promise<{
8182
isLivePreviewEnabled?: boolean
8283
livePreviewConfig?: LivePreviewConfig
83-
livePreviewURL?: string
84+
livePreviewURL?: LivePreviewURLType
8485
}> => {
8586
const collectionConfig = collectionSlug
8687
? req.payload.collections[collectionSlug]?.config

packages/ui/src/views/Edit/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export function DefaultEditView({
143143
previewWindowType,
144144
setURL: setLivePreviewURL,
145145
typeofLivePreviewURL,
146+
url: livePreviewURL,
146147
} = useLivePreviewContext()
147148

148149
const abortOnChangeRef = useRef<AbortController>(null)
@@ -353,7 +354,7 @@ export function DefaultEditView({
353354
setDocumentIsLocked(false)
354355
}
355356

356-
if (livePreviewURL) {
357+
if (isLivePreviewEnabled && typeofLivePreviewURL === 'function') {
357358
setLivePreviewURL(livePreviewURL)
358359
}
359360

@@ -692,7 +693,7 @@ export function DefaultEditView({
692693
/>
693694
{AfterDocument}
694695
</div>
695-
{isLivePreviewEnabled && !isInDrawer && (
696+
{isLivePreviewEnabled && !isInDrawer && livePreviewURL && (
696697
<LivePreviewWindow collectionSlug={collectionSlug} globalSlug={globalSlug} />
697698
)}
698699
</div>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Gutter } from '@payloadcms/ui'
2+
3+
import React, { Fragment } from 'react'
4+
5+
type Args = {
6+
params: Promise<{
7+
slug?: string
8+
}>
9+
}
10+
11+
export default async function TestPage(args: Args) {
12+
return (
13+
<Fragment>
14+
<Gutter>
15+
<p>This is a static page for testing.</p>
16+
</Gutter>
17+
</Fragment>
18+
)
19+
}

0 commit comments

Comments
 (0)