Skip to content

Commit d826159

Browse files
authored
fix(ui): add support back for custom live preview components (#14037)
Fixes #13308 Adds support for a custom live preview component back, we previously supported this and it was allowed via the config types but it wasn't being rendered. Now we export the `useLivePreviewContext` hook and the original `LivePreviewWindow` component too so that end users can wrap the live preview functionality with anything custom that they may need
1 parent 7088d25 commit d826159

File tree

14 files changed

+522
-3
lines changed

14 files changed

+522
-3
lines changed

packages/next/src/views/Document/renderDocumentSlots.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ export const renderDocumentSlots: (args: {
8080
})
8181
}
8282

83+
const LivePreview =
84+
collectionConfig?.admin?.components?.views?.edit?.livePreview ||
85+
globalConfig?.admin?.components?.views?.edit?.livePreview
86+
87+
if (LivePreview?.Component) {
88+
components.LivePreview = RenderServerComponent({
89+
Component: LivePreview.Component,
90+
importMap: req.payload.importMap,
91+
serverProps,
92+
})
93+
}
94+
8395
const descriptionFromConfig =
8496
collectionConfig?.admin?.description || globalConfig?.admin?.description
8597

packages/payload/src/admin/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ export type DocumentSlots = {
561561
BeforeDocumentControls?: React.ReactNode
562562
Description?: React.ReactNode
563563
EditMenuItems?: React.ReactNode
564+
LivePreview?: React.ReactNode
564565
PreviewButton?: React.ReactNode
565566
PublishButton?: React.ReactNode
566567
SaveButton?: React.ReactNode

packages/ui/src/exports/client/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,6 @@ export type {
415415
RenderFieldServerFnArgs,
416416
RenderFieldServerFnReturnType,
417417
} from '../../forms/fieldSchemasToFormState/serverFunctions/renderFieldServerFn.js'
418+
419+
export { useLivePreviewContext } from '../../providers/LivePreview/context.js'
420+
export { LivePreviewWindow } from '../../elements/LivePreview/Window/index.js'

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export function DefaultEditView({
5656
BeforeDocumentControls,
5757
Description,
5858
EditMenuItems,
59+
LivePreview: CustomLivePreview,
5960
PreviewButton,
6061
PublishButton,
6162
SaveButton,
@@ -694,7 +695,11 @@ export function DefaultEditView({
694695
{AfterDocument}
695696
</div>
696697
{isLivePreviewEnabled && !isInDrawer && livePreviewURL && (
697-
<LivePreviewWindow collectionSlug={collectionSlug} globalSlug={globalSlug} />
698+
<>
699+
{CustomLivePreview || (
700+
<LivePreviewWindow collectionSlug={collectionSlug} globalSlug={globalSlug} />
701+
)}
702+
</>
698703
)}
699704
</div>
700705
</Form>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use client'
2+
3+
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
4+
import { useRouter } from 'next/navigation.js'
5+
import React from 'react'
6+
7+
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
8+
9+
export const RefreshRouteOnSave: React.FC = () => {
10+
const router = useRouter()
11+
return <PayloadLivePreview refresh={() => router.refresh()} serverURL={PAYLOAD_SERVER_URL} />
12+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Gutter } from '@payloadcms/ui'
2+
import { notFound } from 'next/navigation.js'
3+
import React, { Fragment } from 'react'
4+
5+
import type { Page } from '../../../../../payload-types.js'
6+
7+
import { renderedPageTitleID, customLivePreviewSlug } from '../../../../../shared.js'
8+
import { getDoc } from '../../../_api/getDoc.js'
9+
import { getDocs } from '../../../_api/getDocs.js'
10+
import { Blocks } from '../../../_components/Blocks/index.js'
11+
import { Hero } from '../../../_components/Hero/index.js'
12+
import { RefreshRouteOnSave } from './RefreshRouteOnSave.js'
13+
14+
type Args = {
15+
params: Promise<{
16+
slug?: string
17+
}>
18+
}
19+
20+
export default async function SSRAutosavePage({ params: paramsPromise }: Args) {
21+
const { slug = '' } = await paramsPromise
22+
23+
const data = await getDoc<Page>({
24+
slug,
25+
collection: customLivePreviewSlug,
26+
draft: true,
27+
})
28+
29+
if (!data) {
30+
notFound()
31+
}
32+
33+
return (
34+
<Fragment>
35+
<RefreshRouteOnSave />
36+
<Hero {...data?.hero} />
37+
<Blocks blocks={data?.layout} />
38+
<Gutter>
39+
<div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div>
40+
</Gutter>
41+
</Fragment>
42+
)
43+
}
44+
45+
export async function generateStaticParams() {
46+
process.env.PAYLOAD_DROP_DATABASE = 'false'
47+
try {
48+
const ssrPages = await getDocs<Page>(customLivePreviewSlug)
49+
return ssrPages?.map((page) => {
50+
return { slug: page.slug }
51+
})
52+
} catch (_err) {
53+
return []
54+
}
55+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
import { Archive } from '../blocks/ArchiveBlock/index.js'
4+
import { CallToAction } from '../blocks/CallToAction/index.js'
5+
import { Content } from '../blocks/Content/index.js'
6+
import { MediaBlock } from '../blocks/MediaBlock/index.js'
7+
import { hero } from '../fields/hero.js'
8+
import { customLivePreviewSlug, mediaSlug, tenantsSlug } from '../shared.js'
9+
10+
export const CustomLivePreview: CollectionConfig = {
11+
slug: customLivePreviewSlug,
12+
labels: {
13+
singular: 'Custom Live Preview Page',
14+
plural: 'Custom Live Preview Pages',
15+
},
16+
access: {
17+
read: () => true,
18+
create: () => true,
19+
update: () => true,
20+
delete: () => true,
21+
},
22+
admin: {
23+
useAsTitle: 'title',
24+
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
25+
preview: (doc) => `/live-preview/ssr/${doc?.slug}`,
26+
components: {
27+
views: {
28+
edit: {
29+
livePreview: {
30+
Component: '/components/CustomLivePreview.js#CustomLivePreview',
31+
},
32+
},
33+
},
34+
},
35+
},
36+
fields: [
37+
{
38+
name: 'slug',
39+
type: 'text',
40+
required: true,
41+
admin: {
42+
position: 'sidebar',
43+
},
44+
},
45+
{
46+
name: 'tenant',
47+
type: 'relationship',
48+
relationTo: tenantsSlug,
49+
admin: {
50+
position: 'sidebar',
51+
},
52+
},
53+
{
54+
name: 'title',
55+
type: 'text',
56+
required: true,
57+
},
58+
{
59+
type: 'tabs',
60+
tabs: [
61+
{
62+
label: 'Hero',
63+
fields: [hero],
64+
},
65+
{
66+
label: 'Content',
67+
fields: [
68+
{
69+
name: 'layout',
70+
type: 'blocks',
71+
blocks: [CallToAction, Content, MediaBlock, Archive],
72+
},
73+
],
74+
},
75+
],
76+
},
77+
{
78+
name: 'meta',
79+
type: 'group',
80+
fields: [
81+
{
82+
name: 'title',
83+
type: 'text',
84+
},
85+
{
86+
name: 'description',
87+
type: 'textarea',
88+
},
89+
{
90+
name: 'image',
91+
type: 'upload',
92+
relationTo: mediaSlug,
93+
},
94+
],
95+
},
96+
],
97+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client'
2+
import { LivePreviewWindow, useDocumentInfo, useLivePreviewContext } from '@payloadcms/ui'
3+
4+
import './styles.css'
5+
6+
export function CustomLivePreview() {
7+
const { collectionSlug, globalSlug } = useDocumentInfo()
8+
const { isLivePreviewing, setURL, url } = useLivePreviewContext()
9+
10+
return (
11+
<div
12+
className={[
13+
'custom-live-preview',
14+
isLivePreviewing && `custom-live-preview--is-live-previewing`,
15+
]
16+
.filter(Boolean)
17+
.join(' ')}
18+
>
19+
{isLivePreviewing && (
20+
<>
21+
<p>Custom live preview being rendered</p>
22+
<LivePreviewWindow collectionSlug={collectionSlug} globalSlug={globalSlug} />
23+
</>
24+
)}
25+
</div>
26+
)
27+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.custom-live-preview {
2+
width: 80%;
3+
display: none;
4+
overflow: hidden;
5+
6+
&.custom-live-preview--is-live-previewing {
7+
display: block;
8+
}
9+
10+
.live-preview-window {
11+
width: 100%;
12+
}
13+
}

test/live-preview/config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
66
import { MediaBlock } from './blocks/MediaBlock/index.js'
77
import { Categories } from './collections/Categories.js'
88
import { CollectionLevelConfig } from './collections/CollectionLevelConfig.js'
9+
import { CustomLivePreview } from './collections/CustomLivePreview.js'
910
import { Media } from './collections/Media.js'
1011
import { NoURLCollection } from './collections/NoURL.js'
1112
import { Pages } from './collections/Pages.js'
@@ -19,6 +20,7 @@ import { Footer } from './globals/Footer.js'
1920
import { Header } from './globals/Header.js'
2021
import { seed } from './seed/index.js'
2122
import {
23+
customLivePreviewSlug,
2224
desktopBreakpoint,
2325
mobileBreakpoint,
2426
pagesSlug,
@@ -42,7 +44,13 @@ export default buildConfigWithDefaults({
4244
// The Live Preview config cascades from the top down, properties are inherited from here
4345
url: formatLivePreviewURL,
4446
breakpoints: [mobileBreakpoint, desktopBreakpoint],
45-
collections: [pagesSlug, postsSlug, ssrPagesSlug, ssrAutosavePagesSlug],
47+
collections: [
48+
pagesSlug,
49+
postsSlug,
50+
ssrPagesSlug,
51+
ssrAutosavePagesSlug,
52+
customLivePreviewSlug,
53+
],
4654
globals: ['header', 'footer'],
4755
},
4856
},
@@ -59,6 +67,7 @@ export default buildConfigWithDefaults({
5967
Media,
6068
CollectionLevelConfig,
6169
StaticURLCollection,
70+
CustomLivePreview,
6271
NoURLCollection,
6372
],
6473
globals: [Header, Footer],

0 commit comments

Comments
 (0)