fix(next): ssr live preview was not dispatching document save events (#6572)
This commit is contained in:
@@ -8,8 +8,10 @@ import { DocumentControls } from '@payloadcms/ui/elements/DocumentControls'
|
||||
import { DocumentFields } from '@payloadcms/ui/elements/DocumentFields'
|
||||
import { Form } from '@payloadcms/ui/forms/Form'
|
||||
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
||||
import { useAuth } from '@payloadcms/ui/providers/Auth'
|
||||
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
|
||||
import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||
import { useDocumentEvents } from '@payloadcms/ui/providers/DocumentEvents'
|
||||
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
|
||||
import { OperationProvider } from '@payloadcms/ui/providers/Operation'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
@@ -71,20 +73,27 @@ const PreviewView: React.FC<Props> = ({
|
||||
|
||||
const operation = id ? 'update' : 'create'
|
||||
|
||||
const {
|
||||
admin: { user: userSlug },
|
||||
} = useConfig()
|
||||
const { t } = useTranslation()
|
||||
const { previewWindowType } = useLivePreviewContext()
|
||||
const { refreshCookieAsync, user } = useAuth()
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
|
||||
const onSave = useCallback(
|
||||
(json) => {
|
||||
// reportUpdate({
|
||||
// id,
|
||||
// entitySlug: collectionConfig.slug,
|
||||
// updatedAt: json?.result?.updatedAt || new Date().toISOString(),
|
||||
// })
|
||||
reportUpdate({
|
||||
id,
|
||||
entitySlug: collectionSlug,
|
||||
updatedAt: json?.result?.updatedAt || new Date().toISOString(),
|
||||
})
|
||||
|
||||
// if (auth && id === user.id) {
|
||||
// await refreshCookieAsync()
|
||||
// }
|
||||
// If we're editing the doc of the logged-in user,
|
||||
// Refresh the cookie to get new permissions
|
||||
if (user && collectionSlug === userSlug && id === user.id) {
|
||||
void refreshCookieAsync()
|
||||
}
|
||||
|
||||
if (typeof onSaveFromProps === 'function') {
|
||||
void onSaveFromProps({
|
||||
@@ -93,12 +102,7 @@ const PreviewView: React.FC<Props> = ({
|
||||
})
|
||||
}
|
||||
},
|
||||
[
|
||||
id,
|
||||
onSaveFromProps,
|
||||
// refreshCookieAsync,
|
||||
// reportUpdate
|
||||
],
|
||||
[collectionSlug, id, onSaveFromProps, refreshCookieAsync, reportUpdate, user, userSlug],
|
||||
)
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,6 @@ export const SSR: CollectionConfig = {
|
||||
update: () => true,
|
||||
delete: () => true,
|
||||
},
|
||||
versions: {
|
||||
drafts: {
|
||||
autosave: {
|
||||
interval: 375,
|
||||
},
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
|
||||
|
||||
94
test/live-preview/collections/SSRAutosave.ts
Normal file
94
test/live-preview/collections/SSRAutosave.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -4,12 +4,19 @@ import { Media } from './collections/Media.js'
|
||||
import { Pages } from './collections/Pages.js'
|
||||
import { Posts } from './collections/Posts.js'
|
||||
import { SSR } from './collections/SSR.js'
|
||||
import { SSRAutosave } from './collections/SSRAutosave.js'
|
||||
import { Tenants } from './collections/Tenants.js'
|
||||
import { Users } from './collections/Users.js'
|
||||
import { Footer } from './globals/Footer.js'
|
||||
import { Header } from './globals/Header.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'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
@@ -19,13 +26,13 @@ export default buildConfigWithDefaults({
|
||||
// The Live Preview config cascades from the top down, properties are inherited from here
|
||||
url: formatLivePreviewURL,
|
||||
breakpoints: [mobileBreakpoint],
|
||||
collections: [pagesSlug, postsSlug, ssrPagesSlug],
|
||||
collections: [pagesSlug, postsSlug, ssrPagesSlug, ssrAutosavePagesSlug],
|
||||
globals: ['header', 'footer'],
|
||||
},
|
||||
},
|
||||
cors: ['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],
|
||||
onInit: seed,
|
||||
})
|
||||
|
||||
@@ -14,7 +14,13 @@ import {
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.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 dirname = path.dirname(filename)
|
||||
|
||||
@@ -25,7 +31,8 @@ describe('Live Preview', () => {
|
||||
let serverURL: string
|
||||
|
||||
let pagesURLUtil: AdminUrlUtil
|
||||
let ssrPostsURLUtil: AdminUrlUtil
|
||||
let ssrPagesURLUtil: AdminUrlUtil
|
||||
let ssrAutosavePostsURLUtil: AdminUrlUtil
|
||||
|
||||
const goToDoc = async (page: Page, urlUtil: AdminUrlUtil) => {
|
||||
await page.goto(urlUtil.list)
|
||||
@@ -51,7 +58,8 @@ describe('Live Preview', () => {
|
||||
;({ serverURL } = await initPayloadE2ENoConfig({ dirname }))
|
||||
|
||||
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()
|
||||
page = await context.newPage()
|
||||
@@ -120,8 +128,38 @@ describe('Live Preview', () => {
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
test('collection — re-render iframe server-side when autosave is made', async () => {
|
||||
await goToCollectionPreview(page, ssrPostsURLUtil)
|
||||
test('collection ssr — re-render iframe when save is made', async () => {
|
||||
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 frame = page.frameLocator('iframe.live-preview-iframe').first()
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Config {
|
||||
pages: Page;
|
||||
posts: Post;
|
||||
ssr: Ssr;
|
||||
'ssr-autosave': SsrAutosave;
|
||||
tenants: Tenant;
|
||||
categories: Category;
|
||||
media: Media;
|
||||
@@ -233,7 +234,7 @@ export interface Page {
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
tab: {
|
||||
tab?: {
|
||||
relationshipInTab?: (string | null) | Post;
|
||||
};
|
||||
meta?: {
|
||||
@@ -271,6 +272,8 @@ export interface Media {
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -544,6 +547,137 @@ export interface Ssr {
|
||||
};
|
||||
updatedAt: 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;
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url'
|
||||
|
||||
import { devUser } from '../../credentials.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 { header } from './header.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({
|
||||
slug: 'header',
|
||||
data: JSON.parse(JSON.stringify(header).replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)),
|
||||
|
||||
@@ -4,6 +4,8 @@ export const tenantsSlug = 'tenants'
|
||||
|
||||
export const ssrPagesSlug = 'ssr'
|
||||
|
||||
export const ssrAutosavePagesSlug = 'ssr-autosave'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const mobileBreakpoint = {
|
||||
|
||||
Reference in New Issue
Block a user