fix(next): ssr live preview was not dispatching document save events (#6572)

This commit is contained in:
Jacob Fletcher
2024-05-30 14:20:22 -04:00
committed by GitHub
parent edfa85bcd5
commit e603c83f55
10 changed files with 377 additions and 31 deletions

View File

@@ -8,8 +8,10 @@ import { DocumentControls } from '@payloadcms/ui/elements/DocumentControls'
import { DocumentFields } from '@payloadcms/ui/elements/DocumentFields' import { DocumentFields } from '@payloadcms/ui/elements/DocumentFields'
import { Form } from '@payloadcms/ui/forms/Form' import { Form } from '@payloadcms/ui/forms/Form'
import { SetViewActions } from '@payloadcms/ui/providers/Actions' import { SetViewActions } from '@payloadcms/ui/providers/Actions'
import { useAuth } from '@payloadcms/ui/providers/Auth'
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap' import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
import { useConfig } from '@payloadcms/ui/providers/Config' import { useConfig } from '@payloadcms/ui/providers/Config'
import { useDocumentEvents } from '@payloadcms/ui/providers/DocumentEvents'
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo' import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
import { OperationProvider } from '@payloadcms/ui/providers/Operation' import { OperationProvider } from '@payloadcms/ui/providers/Operation'
import { useTranslation } from '@payloadcms/ui/providers/Translation' import { useTranslation } from '@payloadcms/ui/providers/Translation'
@@ -71,20 +73,27 @@ const PreviewView: React.FC<Props> = ({
const operation = id ? 'update' : 'create' const operation = id ? 'update' : 'create'
const {
admin: { user: userSlug },
} = useConfig()
const { t } = useTranslation() const { t } = useTranslation()
const { previewWindowType } = useLivePreviewContext() const { previewWindowType } = useLivePreviewContext()
const { refreshCookieAsync, user } = useAuth()
const { reportUpdate } = useDocumentEvents()
const onSave = useCallback( const onSave = useCallback(
(json) => { (json) => {
// reportUpdate({ reportUpdate({
// id, id,
// entitySlug: collectionConfig.slug, entitySlug: collectionSlug,
// updatedAt: json?.result?.updatedAt || new Date().toISOString(), updatedAt: json?.result?.updatedAt || new Date().toISOString(),
// }) })
// if (auth && id === user.id) { // If we're editing the doc of the logged-in user,
// await refreshCookieAsync() // Refresh the cookie to get new permissions
// } if (user && collectionSlug === userSlug && id === user.id) {
void refreshCookieAsync()
}
if (typeof onSaveFromProps === 'function') { if (typeof onSaveFromProps === 'function') {
void onSaveFromProps({ void onSaveFromProps({
@@ -93,12 +102,7 @@ const PreviewView: React.FC<Props> = ({
}) })
} }
}, },
[ [collectionSlug, id, onSaveFromProps, refreshCookieAsync, reportUpdate, user, userSlug],
id,
onSaveFromProps,
// refreshCookieAsync,
// reportUpdate
],
) )
const onChange: FormProps['onChange'][0] = useCallback( const onChange: FormProps['onChange'][0] = useCallback(

View File

@@ -0,0 +1,12 @@
'use client'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation.js'
import React from 'react'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
export const RefreshRouteOnSave: React.FC = () => {
const router = useRouter()
return <PayloadLivePreview refresh={() => router.refresh()} serverURL={PAYLOAD_SERVER_URL} />
}

View File

@@ -0,0 +1,45 @@
import { Gutter } from '@payloadcms/ui/elements/Gutter'
import { notFound } from 'next/navigation.js'
import React, { Fragment } from 'react'
import type { Page } from '../../../../../payload-types.js'
import { renderedPageTitleID, ssrAutosavePagesSlug } from '../../../../../shared.js'
import { getDoc } from '../../../_api/getDoc.js'
import { getDocs } from '../../../_api/getDocs.js'
import { Blocks } from '../../../_components/Blocks/index.js'
import { Hero } from '../../../_components/Hero/index.js'
import { RefreshRouteOnSave } from './RefreshRouteOnSave.js'
export default async function SSRAutosavePage({ params: { slug = '' } }) {
const data = await getDoc<Page>({
slug,
collection: ssrAutosavePagesSlug,
draft: true,
})
if (!data) {
notFound()
}
return (
<Fragment>
<RefreshRouteOnSave />
<Hero {...data?.hero} />
<Blocks blocks={data?.layout} />
<Gutter>
<div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div>
</Gutter>
</Fragment>
)
}
export async function generateStaticParams() {
process.env.PAYLOAD_DROP_DATABASE = 'false'
try {
const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug)
return ssrPages?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -19,13 +19,6 @@ export const SSR: CollectionConfig = {
update: () => true, update: () => true,
delete: () => true, delete: () => true,
}, },
versions: {
drafts: {
autosave: {
interval: 375,
},
},
},
admin: { admin: {
useAsTitle: 'title', useAsTitle: 'title',
defaultColumns: ['id', 'title', 'slug', 'createdAt'], defaultColumns: ['id', 'title', 'slug', 'createdAt'],

View File

@@ -0,0 +1,94 @@
import type { CollectionConfig } from 'payload/types'
import { Archive } from '../blocks/ArchiveBlock/index.js'
import { CallToAction } from '../blocks/CallToAction/index.js'
import { Content } from '../blocks/Content/index.js'
import { MediaBlock } from '../blocks/MediaBlock/index.js'
import { hero } from '../fields/hero.js'
import { ssrAutosavePagesSlug, tenantsSlug } from '../shared.js'
export const SSRAutosave: CollectionConfig = {
slug: ssrAutosavePagesSlug,
labels: {
singular: 'SSR Autosave Page',
plural: 'SSR Autosave Pages',
},
access: {
read: () => true,
create: () => true,
update: () => true,
delete: () => true,
},
versions: {
drafts: {
autosave: {
interval: 375,
},
},
},
admin: {
useAsTitle: 'title',
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
},
fields: [
{
name: 'slug',
type: 'text',
required: true,
admin: {
position: 'sidebar',
},
},
{
name: 'tenant',
type: 'relationship',
relationTo: tenantsSlug,
admin: {
position: 'sidebar',
},
},
{
name: 'title',
type: 'text',
required: true,
},
{
type: 'tabs',
tabs: [
{
label: 'Hero',
fields: [hero],
},
{
label: 'Content',
fields: [
{
name: 'layout',
type: 'blocks',
blocks: [CallToAction, Content, MediaBlock, Archive],
},
],
},
],
},
{
name: 'meta',
type: 'group',
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'textarea',
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
},
],
},
],
}

View File

@@ -4,12 +4,19 @@ import { Media } from './collections/Media.js'
import { Pages } from './collections/Pages.js' import { Pages } from './collections/Pages.js'
import { Posts } from './collections/Posts.js' import { Posts } from './collections/Posts.js'
import { SSR } from './collections/SSR.js' import { SSR } from './collections/SSR.js'
import { SSRAutosave } from './collections/SSRAutosave.js'
import { Tenants } from './collections/Tenants.js' import { Tenants } from './collections/Tenants.js'
import { Users } from './collections/Users.js' import { Users } from './collections/Users.js'
import { Footer } from './globals/Footer.js' import { Footer } from './globals/Footer.js'
import { Header } from './globals/Header.js' import { Header } from './globals/Header.js'
import { seed } from './seed/index.js' import { seed } from './seed/index.js'
import { mobileBreakpoint, pagesSlug, postsSlug, ssrPagesSlug } from './shared.js' import {
mobileBreakpoint,
pagesSlug,
postsSlug,
ssrAutosavePagesSlug,
ssrPagesSlug,
} from './shared.js'
import { formatLivePreviewURL } from './utilities/formatLivePreviewURL.js' import { formatLivePreviewURL } from './utilities/formatLivePreviewURL.js'
export default buildConfigWithDefaults({ export default buildConfigWithDefaults({
@@ -19,13 +26,13 @@ export default buildConfigWithDefaults({
// The Live Preview config cascades from the top down, properties are inherited from here // The Live Preview config cascades from the top down, properties are inherited from here
url: formatLivePreviewURL, url: formatLivePreviewURL,
breakpoints: [mobileBreakpoint], breakpoints: [mobileBreakpoint],
collections: [pagesSlug, postsSlug, ssrPagesSlug], collections: [pagesSlug, postsSlug, ssrPagesSlug, ssrAutosavePagesSlug],
globals: ['header', 'footer'], globals: ['header', 'footer'],
}, },
}, },
cors: ['http://localhost:3000', 'http://localhost:3001'], cors: ['http://localhost:3000', 'http://localhost:3001'],
csrf: ['http://localhost:3000', 'http://localhost:3001'], csrf: ['http://localhost:3000', 'http://localhost:3001'],
collections: [Users, Pages, Posts, SSR, Tenants, Categories, Media], collections: [Users, Pages, Posts, SSR, SSRAutosave, Tenants, Categories, Media],
globals: [Header, Footer], globals: [Header, Footer],
onInit: seed, onInit: seed,
}) })

View File

@@ -14,7 +14,13 @@ import {
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { mobileBreakpoint, pagesSlug, renderedPageTitleID, ssrPagesSlug } from './shared.js' import {
mobileBreakpoint,
pagesSlug,
renderedPageTitleID,
ssrAutosavePagesSlug,
ssrPagesSlug,
} from './shared.js'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@@ -25,7 +31,8 @@ describe('Live Preview', () => {
let serverURL: string let serverURL: string
let pagesURLUtil: AdminUrlUtil let pagesURLUtil: AdminUrlUtil
let ssrPostsURLUtil: AdminUrlUtil let ssrPagesURLUtil: AdminUrlUtil
let ssrAutosavePostsURLUtil: AdminUrlUtil
const goToDoc = async (page: Page, urlUtil: AdminUrlUtil) => { const goToDoc = async (page: Page, urlUtil: AdminUrlUtil) => {
await page.goto(urlUtil.list) await page.goto(urlUtil.list)
@@ -51,7 +58,8 @@ describe('Live Preview', () => {
;({ serverURL } = await initPayloadE2ENoConfig({ dirname })) ;({ serverURL } = await initPayloadE2ENoConfig({ dirname }))
pagesURLUtil = new AdminUrlUtil(serverURL, pagesSlug) pagesURLUtil = new AdminUrlUtil(serverURL, pagesSlug)
ssrPostsURLUtil = new AdminUrlUtil(serverURL, ssrPagesSlug) ssrPagesURLUtil = new AdminUrlUtil(serverURL, ssrPagesSlug)
ssrAutosavePostsURLUtil = new AdminUrlUtil(serverURL, ssrAutosavePagesSlug)
const context = await browser.newContext() const context = await browser.newContext()
page = await context.newPage() page = await context.newPage()
@@ -120,8 +128,38 @@ describe('Live Preview', () => {
await saveDocAndAssert(page) await saveDocAndAssert(page)
}) })
test('collection — re-render iframe server-side when autosave is made', async () => { test('collection ssr — re-render iframe when save is made', async () => {
await goToCollectionPreview(page, ssrPostsURLUtil) await goToCollectionPreview(page, ssrPagesURLUtil)
const titleField = page.locator('#field-title')
const frame = page.frameLocator('iframe.live-preview-iframe').first()
await expect(titleField).toBeVisible()
const renderedPageTitleLocator = `#${renderedPageTitleID}`
// Forces the test to wait for the Next.js route to render before we try editing a field
await expect(() => expect(frame.locator(renderedPageTitleLocator)).toBeVisible()).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
await expect(frame.locator(renderedPageTitleLocator)).toHaveText('For Testing: SSR Home')
const newTitleValue = 'SSR Home (Edited)'
await titleField.fill(newTitleValue)
await saveDocAndAssert(page)
await expect(() =>
expect(frame.locator(renderedPageTitleLocator)).toHaveText(`For Testing: ${newTitleValue}`),
).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('collection ssr — re-render iframe when autosave is made', async () => {
await goToCollectionPreview(page, ssrAutosavePostsURLUtil)
const titleField = page.locator('#field-title') const titleField = page.locator('#field-title')
const frame = page.frameLocator('iframe.live-preview-iframe').first() const frame = page.frameLocator('iframe.live-preview-iframe').first()

View File

@@ -12,6 +12,7 @@ export interface Config {
pages: Page; pages: Page;
posts: Post; posts: Post;
ssr: Ssr; ssr: Ssr;
'ssr-autosave': SsrAutosave;
tenants: Tenant; tenants: Tenant;
categories: Category; categories: Category;
media: Media; media: Media;
@@ -233,7 +234,7 @@ export interface Page {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
tab: { tab?: {
relationshipInTab?: (string | null) | Post; relationshipInTab?: (string | null) | Post;
}; };
meta?: { meta?: {
@@ -271,6 +272,8 @@ export interface Media {
filesize?: number | null; filesize?: number | null;
width?: number | null; width?: number | null;
height?: number | null; height?: number | null;
focalX?: number | null;
focalY?: number | null;
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
@@ -544,6 +547,137 @@ export interface Ssr {
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ssr-autosave".
*/
export interface SsrAutosave {
id: string;
slug: string;
tenant?: (string | null) | Tenant;
title: string;
hero: {
type: 'none' | 'highImpact' | 'lowImpact';
richText?:
| {
[k: string]: unknown;
}[]
| null;
media?: string | Media | null;
};
layout?:
| (
| {
invertBackground?: boolean | null;
richText?:
| {
[k: string]: unknown;
}[]
| null;
links?:
| {
link: {
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?:
| ({
relationTo: 'posts';
value: string | Post;
} | null)
| ({
relationTo: 'pages';
value: string | Page;
} | null);
url?: string | null;
label: string;
appearance?: ('primary' | 'secondary') | null;
};
id?: string | null;
}[]
| null;
id?: string | null;
blockName?: string | null;
blockType: 'cta';
}
| {
invertBackground?: boolean | null;
columns?:
| {
size?: ('oneThird' | 'half' | 'twoThirds' | 'full') | null;
richText?:
| {
[k: string]: unknown;
}[]
| null;
enableLink?: boolean | null;
link?: {
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?:
| ({
relationTo: 'posts';
value: string | Post;
} | null)
| ({
relationTo: 'pages';
value: string | Page;
} | null);
url?: string | null;
label: string;
appearance?: ('default' | 'primary' | 'secondary') | null;
};
id?: string | null;
}[]
| null;
id?: string | null;
blockName?: string | null;
blockType: 'content';
}
| {
invertBackground?: boolean | null;
position?: ('default' | 'fullscreen') | null;
media: string | Media;
id?: string | null;
blockName?: string | null;
blockType: 'mediaBlock';
}
| {
introContent?:
| {
[k: string]: unknown;
}[]
| null;
populateBy?: ('collection' | 'selection') | null;
relationTo?: 'posts' | null;
categories?: (string | Category)[] | null;
limit?: number | null;
selectedDocs?:
| {
relationTo: 'posts';
value: string | Post;
}[]
| null;
populatedDocs?:
| {
relationTo: 'posts';
value: string | Post;
}[]
| null;
populatedDocsTotal?: number | null;
id?: string | null;
blockName?: string | null;
blockType: 'archive';
}
)[]
| null;
meta?: {
title?: string | null;
description?: string | null;
image?: string | Media | null;
};
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null; _status?: ('draft' | 'published') | null;
} }
/** /**

View File

@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url'
import { devUser } from '../../credentials.js' import { devUser } from '../../credentials.js'
import removeFiles from '../../helpers/removeFiles.js' import removeFiles from '../../helpers/removeFiles.js'
import { pagesSlug, postsSlug, ssrPagesSlug, tenantsSlug } from '../shared.js' import { pagesSlug, postsSlug, ssrAutosavePagesSlug, ssrPagesSlug, tenantsSlug } from '../shared.js'
import { footer } from './footer.js' import { footer } from './footer.js'
import { header } from './header.js' import { header } from './header.js'
import { home } from './home.js' import { home } from './home.js'
@@ -125,6 +125,23 @@ export const seed: Config['onInit'] = async (payload) => {
}, },
}) })
await payload.create({
collection: ssrAutosavePagesSlug,
data: {
...JSON.parse(
JSON.stringify(home)
.replace(/"\{\{MEDIA_ID\}\}"/g, mediaID)
.replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)
.replace(/"\{\{POST_1_ID\}\}"/g, post1DocID)
.replace(/"\{\{POST_2_ID\}\}"/g, post2DocID)
.replace(/"\{\{POST_3_ID\}\}"/g, post3DocID)
.replace(/"\{\{TENANT_1_ID\}\}"/g, tenantID),
),
title: 'SSR Home',
slug: 'home',
},
})
await payload.updateGlobal({ await payload.updateGlobal({
slug: 'header', slug: 'header',
data: JSON.parse(JSON.stringify(header).replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)), data: JSON.parse(JSON.stringify(header).replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)),

View File

@@ -4,6 +4,8 @@ export const tenantsSlug = 'tenants'
export const ssrPagesSlug = 'ssr' export const ssrPagesSlug = 'ssr'
export const ssrAutosavePagesSlug = 'ssr-autosave'
export const postsSlug = 'posts' export const postsSlug = 'posts'
export const mobileBreakpoint = { export const mobileBreakpoint = {