Skip to content

Commit e603c83

Browse files
authored
fix(next): ssr live preview was not dispatching document save events (#6572)
1 parent edfa85b commit e603c83

File tree

10 files changed

+377
-31
lines changed

10 files changed

+377
-31
lines changed

packages/next/src/views/LivePreview/index.client.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { DocumentControls } from '@payloadcms/ui/elements/DocumentControls'
88
import { DocumentFields } from '@payloadcms/ui/elements/DocumentFields'
99
import { Form } from '@payloadcms/ui/forms/Form'
1010
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
11+
import { useAuth } from '@payloadcms/ui/providers/Auth'
1112
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
1213
import { useConfig } from '@payloadcms/ui/providers/Config'
14+
import { useDocumentEvents } from '@payloadcms/ui/providers/DocumentEvents'
1315
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
1416
import { OperationProvider } from '@payloadcms/ui/providers/Operation'
1517
import { useTranslation } from '@payloadcms/ui/providers/Translation'
@@ -71,20 +73,27 @@ const PreviewView: React.FC<Props> = ({
7173

7274
const operation = id ? 'update' : 'create'
7375

76+
const {
77+
admin: { user: userSlug },
78+
} = useConfig()
7479
const { t } = useTranslation()
7580
const { previewWindowType } = useLivePreviewContext()
81+
const { refreshCookieAsync, user } = useAuth()
82+
const { reportUpdate } = useDocumentEvents()
7683

7784
const onSave = useCallback(
7885
(json) => {
79-
// reportUpdate({
80-
// id,
81-
// entitySlug: collectionConfig.slug,
82-
// updatedAt: json?.result?.updatedAt || new Date().toISOString(),
83-
// })
86+
reportUpdate({
87+
id,
88+
entitySlug: collectionSlug,
89+
updatedAt: json?.result?.updatedAt || new Date().toISOString(),
90+
})
8491

85-
// if (auth && id === user.id) {
86-
// await refreshCookieAsync()
87-
// }
92+
// If we're editing the doc of the logged-in user,
93+
// Refresh the cookie to get new permissions
94+
if (user && collectionSlug === userSlug && id === user.id) {
95+
void refreshCookieAsync()
96+
}
8897

8998
if (typeof onSaveFromProps === 'function') {
9099
void onSaveFromProps({
@@ -93,12 +102,7 @@ const PreviewView: React.FC<Props> = ({
93102
})
94103
}
95104
},
96-
[
97-
id,
98-
onSaveFromProps,
99-
// refreshCookieAsync,
100-
// reportUpdate
101-
],
105+
[collectionSlug, id, onSaveFromProps, refreshCookieAsync, reportUpdate, user, userSlug],
102106
)
103107

104108
const onChange: FormProps['onChange'][0] = useCallback(
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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Gutter } from '@payloadcms/ui/elements/Gutter'
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, ssrAutosavePagesSlug } 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+
export default async function SSRAutosavePage({ params: { slug = '' } }) {
15+
const data = await getDoc<Page>({
16+
slug,
17+
collection: ssrAutosavePagesSlug,
18+
draft: true,
19+
})
20+
21+
if (!data) {
22+
notFound()
23+
}
24+
25+
return (
26+
<Fragment>
27+
<RefreshRouteOnSave />
28+
<Hero {...data?.hero} />
29+
<Blocks blocks={data?.layout} />
30+
<Gutter>
31+
<div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div>
32+
</Gutter>
33+
</Fragment>
34+
)
35+
}
36+
37+
export async function generateStaticParams() {
38+
process.env.PAYLOAD_DROP_DATABASE = 'false'
39+
try {
40+
const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug)
41+
return ssrPages?.map(({ slug }) => slug)
42+
} catch (error) {
43+
return []
44+
}
45+
}

test/live-preview/collections/SSR.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ export const SSR: CollectionConfig = {
1919
update: () => true,
2020
delete: () => true,
2121
},
22-
versions: {
23-
drafts: {
24-
autosave: {
25-
interval: 375,
26-
},
27-
},
28-
},
2922
admin: {
3023
useAsTitle: 'title',
3124
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { CollectionConfig } from 'payload/types'
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 { ssrAutosavePagesSlug, tenantsSlug } from '../shared.js'
9+
10+
export const SSRAutosave: CollectionConfig = {
11+
slug: ssrAutosavePagesSlug,
12+
labels: {
13+
singular: 'SSR Autosave Page',
14+
plural: 'SSR Autosave Pages',
15+
},
16+
access: {
17+
read: () => true,
18+
create: () => true,
19+
update: () => true,
20+
delete: () => true,
21+
},
22+
versions: {
23+
drafts: {
24+
autosave: {
25+
interval: 375,
26+
},
27+
},
28+
},
29+
admin: {
30+
useAsTitle: 'title',
31+
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
32+
},
33+
fields: [
34+
{
35+
name: 'slug',
36+
type: 'text',
37+
required: true,
38+
admin: {
39+
position: 'sidebar',
40+
},
41+
},
42+
{
43+
name: 'tenant',
44+
type: 'relationship',
45+
relationTo: tenantsSlug,
46+
admin: {
47+
position: 'sidebar',
48+
},
49+
},
50+
{
51+
name: 'title',
52+
type: 'text',
53+
required: true,
54+
},
55+
{
56+
type: 'tabs',
57+
tabs: [
58+
{
59+
label: 'Hero',
60+
fields: [hero],
61+
},
62+
{
63+
label: 'Content',
64+
fields: [
65+
{
66+
name: 'layout',
67+
type: 'blocks',
68+
blocks: [CallToAction, Content, MediaBlock, Archive],
69+
},
70+
],
71+
},
72+
],
73+
},
74+
{
75+
name: 'meta',
76+
type: 'group',
77+
fields: [
78+
{
79+
name: 'title',
80+
type: 'text',
81+
},
82+
{
83+
name: 'description',
84+
type: 'textarea',
85+
},
86+
{
87+
name: 'image',
88+
type: 'upload',
89+
relationTo: 'media',
90+
},
91+
],
92+
},
93+
],
94+
}

test/live-preview/config.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ import { Media } from './collections/Media.js'
44
import { Pages } from './collections/Pages.js'
55
import { Posts } from './collections/Posts.js'
66
import { SSR } from './collections/SSR.js'
7+
import { SSRAutosave } from './collections/SSRAutosave.js'
78
import { Tenants } from './collections/Tenants.js'
89
import { Users } from './collections/Users.js'
910
import { Footer } from './globals/Footer.js'
1011
import { Header } from './globals/Header.js'
1112
import { seed } from './seed/index.js'
12-
import { mobileBreakpoint, pagesSlug, postsSlug, ssrPagesSlug } from './shared.js'
13+
import {
14+
mobileBreakpoint,
15+
pagesSlug,
16+
postsSlug,
17+
ssrAutosavePagesSlug,
18+
ssrPagesSlug,
19+
} from './shared.js'
1320
import { formatLivePreviewURL } from './utilities/formatLivePreviewURL.js'
1421

1522
export default buildConfigWithDefaults({
@@ -19,13 +26,13 @@ export default buildConfigWithDefaults({
1926
// The Live Preview config cascades from the top down, properties are inherited from here
2027
url: formatLivePreviewURL,
2128
breakpoints: [mobileBreakpoint],
22-
collections: [pagesSlug, postsSlug, ssrPagesSlug],
29+
collections: [pagesSlug, postsSlug, ssrPagesSlug, ssrAutosavePagesSlug],
2330
globals: ['header', 'footer'],
2431
},
2532
},
2633
cors: ['http://localhost:3000', 'http://localhost:3001'],
2734
csrf: ['http://localhost:3000', 'http://localhost:3001'],
28-
collections: [Users, Pages, Posts, SSR, Tenants, Categories, Media],
35+
collections: [Users, Pages, Posts, SSR, SSRAutosave, Tenants, Categories, Media],
2936
globals: [Header, Footer],
3037
onInit: seed,
3138
})

test/live-preview/e2e.spec.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ import {
1414
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
1515
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
1616
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
17-
import { mobileBreakpoint, pagesSlug, renderedPageTitleID, ssrPagesSlug } from './shared.js'
17+
import {
18+
mobileBreakpoint,
19+
pagesSlug,
20+
renderedPageTitleID,
21+
ssrAutosavePagesSlug,
22+
ssrPagesSlug,
23+
} from './shared.js'
1824
const filename = fileURLToPath(import.meta.url)
1925
const dirname = path.dirname(filename)
2026

@@ -25,7 +31,8 @@ describe('Live Preview', () => {
2531
let serverURL: string
2632

2733
let pagesURLUtil: AdminUrlUtil
28-
let ssrPostsURLUtil: AdminUrlUtil
34+
let ssrPagesURLUtil: AdminUrlUtil
35+
let ssrAutosavePostsURLUtil: AdminUrlUtil
2936

3037
const goToDoc = async (page: Page, urlUtil: AdminUrlUtil) => {
3138
await page.goto(urlUtil.list)
@@ -51,7 +58,8 @@ describe('Live Preview', () => {
5158
;({ serverURL } = await initPayloadE2ENoConfig({ dirname }))
5259

5360
pagesURLUtil = new AdminUrlUtil(serverURL, pagesSlug)
54-
ssrPostsURLUtil = new AdminUrlUtil(serverURL, ssrPagesSlug)
61+
ssrPagesURLUtil = new AdminUrlUtil(serverURL, ssrPagesSlug)
62+
ssrAutosavePostsURLUtil = new AdminUrlUtil(serverURL, ssrAutosavePagesSlug)
5563

5664
const context = await browser.newContext()
5765
page = await context.newPage()
@@ -120,8 +128,38 @@ describe('Live Preview', () => {
120128
await saveDocAndAssert(page)
121129
})
122130

123-
test('collection — re-render iframe server-side when autosave is made', async () => {
124-
await goToCollectionPreview(page, ssrPostsURLUtil)
131+
test('collection ssr — re-render iframe when save is made', async () => {
132+
await goToCollectionPreview(page, ssrPagesURLUtil)
133+
134+
const titleField = page.locator('#field-title')
135+
const frame = page.frameLocator('iframe.live-preview-iframe').first()
136+
137+
await expect(titleField).toBeVisible()
138+
139+
const renderedPageTitleLocator = `#${renderedPageTitleID}`
140+
141+
// Forces the test to wait for the Next.js route to render before we try editing a field
142+
await expect(() => expect(frame.locator(renderedPageTitleLocator)).toBeVisible()).toPass({
143+
timeout: POLL_TOPASS_TIMEOUT,
144+
})
145+
146+
await expect(frame.locator(renderedPageTitleLocator)).toHaveText('For Testing: SSR Home')
147+
148+
const newTitleValue = 'SSR Home (Edited)'
149+
150+
await titleField.fill(newTitleValue)
151+
152+
await saveDocAndAssert(page)
153+
154+
await expect(() =>
155+
expect(frame.locator(renderedPageTitleLocator)).toHaveText(`For Testing: ${newTitleValue}`),
156+
).toPass({
157+
timeout: POLL_TOPASS_TIMEOUT,
158+
})
159+
})
160+
161+
test('collection ssr — re-render iframe when autosave is made', async () => {
162+
await goToCollectionPreview(page, ssrAutosavePostsURLUtil)
125163

126164
const titleField = page.locator('#field-title')
127165
const frame = page.frameLocator('iframe.live-preview-iframe').first()

0 commit comments

Comments
 (0)