fix(next): static live preview url corrupt after save (#13949)

As of #13631, statically defined live preview URLs become corrupt after
the first save.

For example, if you define your URL as a string like this:

```ts
import type { CollectionConfig } from 'payload'

const MyCollection: CollectionConfig = {
  // ...
  admin: {
    livePreview: {
      url: '/hello-world'
    }
  }
}
```

On initial load, the iframe's src will evaluate to `.../hello-world` as
expected, but from the first save onward, the url becomes
`.../undefined`.

This is because for statically defined URLs, the `livePreviewURL`
property does not exist on the response. Despite this, we set it into
state as undefined. This is true for both collections and globals.

Initially reported on Discord here:
https://discord.com/channels/967097582721572934/967097582721572937/1421166976113442847

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211478736160152
This commit is contained in:
Jacob Fletcher
2025-09-26 17:23:04 -04:00
committed by GitHub
parent ae34b6d6d1
commit 3c4f8a3508
5 changed files with 62 additions and 4 deletions

View File

@@ -353,7 +353,7 @@ export function DefaultEditView({
setDocumentIsLocked(false)
}
if (isLivePreviewEnabled) {
if (livePreviewURL) {
setLivePreviewURL(livePreviewURL)
}

View File

@@ -0,0 +1,16 @@
import type { CollectionConfig } from 'payload'
export const StaticURLCollection: CollectionConfig = {
slug: 'static-url',
admin: {
livePreview: {
url: '/live-preview/hello-world',
},
},
fields: [
{
name: 'title',
type: 'text',
},
],
}

View File

@@ -11,6 +11,7 @@ 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 { StaticURLCollection } from './collections/StaticURL.js'
import { Tenants } from './collections/Tenants.js'
import { Users } from './collections/Users.js'
import { Footer } from './globals/Footer.js'
@@ -56,6 +57,7 @@ export default buildConfigWithDefaults({
Categories,
Media,
CollectionLevelConfig,
StaticURLCollection,
],
globals: [Header, Footer],
onInit: seed,

View File

@@ -164,6 +164,21 @@ describe('Live Preview', () => {
await expect.poll(async () => iframe.getAttribute('src')).toMatch(/\/live-preview/)
})
test('collection — retains static URL across edits', async () => {
const util = new AdminUrlUtil(serverURL, 'static-url')
await page.goto(util.create)
await saveDocAndAssert(page)
await toggleLivePreview(page, { targetState: 'on' })
const iframe = page.locator('iframe.live-preview-iframe')
await expect.poll(async () => iframe.getAttribute('src')).toMatch(/\/live-preview\/hello/)
const titleField = page.locator('#field-title')
await titleField.fill('New Title')
await saveDocAndAssert(page)
await expect.poll(async () => iframe.getAttribute('src')).toMatch(/\/live-preview\/hello/)
})
test('collection csr — iframe reflects form state on change', async () => {
await goToCollectionLivePreview(page, pagesURLUtil)

View File

@@ -78,6 +78,7 @@ export interface Config {
categories: Category;
media: Media;
'collection-level-config': CollectionLevelConfig;
'static-url': StaticUrl;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -93,6 +94,7 @@ export interface Config {
categories: CategoriesSelect<false> | CategoriesSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
'collection-level-config': CollectionLevelConfigSelect<false> | CollectionLevelConfigSelect<true>;
'static-url': StaticUrlSelect<false> | StaticUrlSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -336,7 +338,7 @@ export interface Page {
root: {
type: string;
children: {
type: string;
type: any;
version: number;
[k: string]: unknown;
}[];
@@ -351,7 +353,7 @@ export interface Page {
root: {
type: string;
children: {
type: string;
type: any;
version: number;
[k: string]: unknown;
}[];
@@ -382,7 +384,7 @@ export interface Page {
root: {
type: string;
children: {
type: string;
type: any;
version: number;
[k: string]: unknown;
}[];
@@ -885,6 +887,16 @@ export interface CollectionLevelConfig {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "static-url".
*/
export interface StaticUrl {
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -927,6 +939,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'collection-level-config';
value: string | CollectionLevelConfig;
} | null)
| ({
relationTo: 'static-url';
value: string | StaticUrl;
} | null);
globalSlug?: string | null;
user: {
@@ -1468,6 +1484,15 @@ export interface CollectionLevelConfigSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "static-url_select".
*/
export interface StaticUrlSelect<T extends boolean = true> {
title?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".