From e603c83f552559e39a97b6bff80670e33765dc24 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Thu, 30 May 2024 14:20:22 -0400 Subject: [PATCH] fix(next): ssr live preview was not dispatching document save events (#6572) --- .../src/views/LivePreview/index.client.tsx | 32 +++-- .../[slug]/RefreshRouteOnSave.tsx | 12 ++ .../(pages)/ssr-autosave/[slug]/page.tsx | 45 ++++++ test/live-preview/collections/SSR.ts | 7 - test/live-preview/collections/SSRAutosave.ts | 94 ++++++++++++ test/live-preview/config.ts | 13 +- test/live-preview/e2e.spec.ts | 48 ++++++- test/live-preview/payload-types.ts | 136 +++++++++++++++++- test/live-preview/seed/index.ts | 19 ++- test/live-preview/shared.ts | 2 + 10 files changed, 377 insertions(+), 31 deletions(-) create mode 100644 test/live-preview/app/live-preview/(pages)/ssr-autosave/[slug]/RefreshRouteOnSave.tsx create mode 100644 test/live-preview/app/live-preview/(pages)/ssr-autosave/[slug]/page.tsx create mode 100644 test/live-preview/collections/SSRAutosave.ts diff --git a/packages/next/src/views/LivePreview/index.client.tsx b/packages/next/src/views/LivePreview/index.client.tsx index f3ebd9e0c..e2478f27e 100644 --- a/packages/next/src/views/LivePreview/index.client.tsx +++ b/packages/next/src/views/LivePreview/index.client.tsx @@ -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 = ({ 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 = ({ }) } }, - [ - id, - onSaveFromProps, - // refreshCookieAsync, - // reportUpdate - ], + [collectionSlug, id, onSaveFromProps, refreshCookieAsync, reportUpdate, user, userSlug], ) const onChange: FormProps['onChange'][0] = useCallback( diff --git a/test/live-preview/app/live-preview/(pages)/ssr-autosave/[slug]/RefreshRouteOnSave.tsx b/test/live-preview/app/live-preview/(pages)/ssr-autosave/[slug]/RefreshRouteOnSave.tsx new file mode 100644 index 000000000..046ec440b --- /dev/null +++ b/test/live-preview/app/live-preview/(pages)/ssr-autosave/[slug]/RefreshRouteOnSave.tsx @@ -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 router.refresh()} serverURL={PAYLOAD_SERVER_URL} /> +} diff --git a/test/live-preview/app/live-preview/(pages)/ssr-autosave/[slug]/page.tsx b/test/live-preview/app/live-preview/(pages)/ssr-autosave/[slug]/page.tsx new file mode 100644 index 000000000..b96c01222 --- /dev/null +++ b/test/live-preview/app/live-preview/(pages)/ssr-autosave/[slug]/page.tsx @@ -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({ + slug, + collection: ssrAutosavePagesSlug, + draft: true, + }) + + if (!data) { + notFound() + } + + return ( + + + + + +
{`For Testing: ${data.title}`}
+
+
+ ) +} + +export async function generateStaticParams() { + process.env.PAYLOAD_DROP_DATABASE = 'false' + try { + const ssrPages = await getDocs(ssrAutosavePagesSlug) + return ssrPages?.map(({ slug }) => slug) + } catch (error) { + return [] + } +} diff --git a/test/live-preview/collections/SSR.ts b/test/live-preview/collections/SSR.ts index 1ceabe5c7..e334fb7ea 100644 --- a/test/live-preview/collections/SSR.ts +++ b/test/live-preview/collections/SSR.ts @@ -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'], diff --git a/test/live-preview/collections/SSRAutosave.ts b/test/live-preview/collections/SSRAutosave.ts new file mode 100644 index 000000000..4925f06d5 --- /dev/null +++ b/test/live-preview/collections/SSRAutosave.ts @@ -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', + }, + ], + }, + ], +} diff --git a/test/live-preview/config.ts b/test/live-preview/config.ts index 452806499..f7e2510bd 100644 --- a/test/live-preview/config.ts +++ b/test/live-preview/config.ts @@ -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, }) diff --git a/test/live-preview/e2e.spec.ts b/test/live-preview/e2e.spec.ts index d8f6fc982..f28fc2e0b 100644 --- a/test/live-preview/e2e.spec.ts +++ b/test/live-preview/e2e.spec.ts @@ -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() diff --git a/test/live-preview/payload-types.ts b/test/live-preview/payload-types.ts index 5e7a9582e..fa090edcc 100644 --- a/test/live-preview/payload-types.ts +++ b/test/live-preview/payload-types.ts @@ -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; } /** diff --git a/test/live-preview/seed/index.ts b/test/live-preview/seed/index.ts index 3bb557192..0e0faca9b 100644 --- a/test/live-preview/seed/index.ts +++ b/test/live-preview/seed/index.ts @@ -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)), diff --git a/test/live-preview/shared.ts b/test/live-preview/shared.ts index 80836c670..16278989c 100644 --- a/test/live-preview/shared.ts +++ b/test/live-preview/shared.ts @@ -4,6 +4,8 @@ export const tenantsSlug = 'tenants' export const ssrPagesSlug = 'ssr' +export const ssrAutosavePagesSlug = 'ssr-autosave' + export const postsSlug = 'posts' export const mobileBreakpoint = {