Skip to content

Commit f2213e5

Browse files
authored
feat: mount live preview to document root (#12860)
Mounts live preview to `../:id` instead `../:id/preview`. This is a huge win for both UX and a maintainability standpoint. Here are just a few of those wins: 1. If you edit a document, _then_ decide you want to preview those changes, you are currently presented with the `LeaveWithoutSaving` modal and are forced to either save your edits or clear them. This is because you are being navigated to an entirely new page with it's own form context. Instead, you should be able to freely navigate back and forth between the two. 2. If you are an editor who most often uses Live Preview, or you are editing a collection that typically requires it, you likely want it to automatically enter live preview mode when you open a document. Currently, the user has to navigate to the document _first_, then use the live preview tab. Instead, you should be able to set a preference and avoid this extra step. 3. Since the inception of Live Preview, we've been maintaining largely the same code across the default edit view and the live preview view, which often became out of sync and inconsistent—but they're essentially doing the same thing. While we could abstract a lot of this out, it is no longer necessary if the two views are combined into one. This change does also include some small modifications to UI. The "Live Preview" tab no longer exists, and instead has been replaced with a button placed next to the document controls (subject to change). Before: https://github.com/user-attachments/assets/48518b02-87ba-4750-ba7b-b21b5c75240a After: https://github.com/user-attachments/assets/a8ec8657-a6d6-4ee1-b9a7-3c1173bcfa96
1 parent 6f6d305 commit f2213e5

File tree

109 files changed

+815
-1135
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+815
-1135
lines changed

docs/custom-components/document-views.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export const MyCollectionOrGlobalConfig: CollectionConfig = {
3030
// - api
3131
// - versions
3232
// - version
33-
// - livePreview
3433
// - [key: string]
3534
// See below for more details
3635
},

packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,6 @@ export const getTabs = ({
2828
},
2929
viewPath: '/',
3030
},
31-
{
32-
tab: {
33-
condition: ({ collectionConfig, config, globalConfig }) => {
34-
if (collectionConfig) {
35-
return Boolean(
36-
config?.admin?.livePreview?.collections?.includes(collectionConfig.slug) ||
37-
collectionConfig?.admin?.livePreview,
38-
)
39-
}
40-
41-
if (globalConfig) {
42-
return Boolean(
43-
config?.admin?.livePreview?.globals?.includes(globalConfig.slug) ||
44-
globalConfig?.admin?.livePreview,
45-
)
46-
}
47-
48-
return false
49-
},
50-
href: '/preview',
51-
label: ({ t }) => t('general:livePreview'),
52-
order: 200,
53-
...(customViews?.['livePreview']?.tab || {}),
54-
},
55-
viewPath: '/preview',
56-
},
5731
{
5832
tab: {
5933
condition: ({ collectionConfig, globalConfig, permissions }) =>

packages/next/src/views/Document/getDocumentView.tsx

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { ViewToRender } from './index.js'
1212

1313
import { APIView as DefaultAPIView } from '../API/index.js'
1414
import { EditView as DefaultEditView } from '../Edit/index.js'
15-
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
1615
import { UnauthorizedViewWithGutter } from '../Unauthorized/index.js'
1716
import { VersionView as DefaultVersionView } from '../Version/index.js'
1817
import { VersionsView as DefaultVersionsView } from '../Versions/index.js'
@@ -112,7 +111,6 @@ export const getDocumentView = ({
112111
}
113112

114113
// --> /collections/:collectionSlug/:id/api
115-
// --> /collections/:collectionSlug/:id/preview
116114
// --> /collections/:collectionSlug/:id/versions
117115
// --> /collections/:collectionSlug/:id/<custom-segment>
118116
case 4: {
@@ -125,17 +123,6 @@ export const getDocumentView = ({
125123
break
126124
}
127125

128-
case 'preview': {
129-
// --> /collections/:collectionSlug/:id/preview
130-
if (
131-
(collectionConfig && collectionConfig?.admin?.livePreview) ||
132-
config?.admin?.livePreview?.collections?.includes(collectionConfig?.slug)
133-
) {
134-
View = getCustomViewByKey(views, 'livePreview') || DefaultLivePreviewView
135-
}
136-
break
137-
}
138-
139126
case 'versions': {
140127
// --> /collections/:collectionSlug/:id/versions
141128
if (docPermissions?.readVersions) {
@@ -234,7 +221,6 @@ export const getDocumentView = ({
234221

235222
case 3: {
236223
// --> /globals/:globalSlug/api
237-
// --> /globals/:globalSlug/preview
238224
// --> /globals/:globalSlug/versions
239225
// --> /globals/:globalSlug/<custom-segment>
240226
switch (segment3) {
@@ -247,18 +233,6 @@ export const getDocumentView = ({
247233
break
248234
}
249235

250-
case 'preview': {
251-
// --> /globals/:globalSlug/preview
252-
if (
253-
(globalConfig && globalConfig?.admin?.livePreview) ||
254-
config?.admin?.livePreview?.globals?.includes(globalConfig?.slug)
255-
) {
256-
View = getCustomViewByKey(views, 'livePreview') || DefaultLivePreviewView
257-
}
258-
259-
break
260-
}
261-
262236
case 'versions': {
263237
// --> /globals/:globalSlug/versions
264238
if (docPermissions?.readVersions) {

packages/next/src/views/Document/getMetaBySegment.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { GenerateViewMetadata } from '../Root/index.js'
66
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
77
import { generateAPIViewMetadata } from '../API/metadata.js'
88
import { generateEditViewMetadata } from '../Edit/metadata.js'
9-
import { generateLivePreviewViewMetadata } from '../LivePreview/metadata.js'
109
import { generateNotFoundViewMetadata } from '../NotFound/metadata.js'
1110
import { generateVersionViewMetadata } from '../Version/metadata.js'
1211
import { generateVersionsViewMetadata } from '../Versions/metadata.js'
@@ -50,10 +49,6 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
5049
// `/:collection/:id/api`
5150
fn = generateAPIViewMetadata
5251
break
53-
case 'preview':
54-
// `/:collection/:id/preview`
55-
fn = generateLivePreviewViewMetadata
56-
break
5752
case 'versions':
5853
// `/:collection/:id/versions`
5954
fn = generateVersionsViewMetadata
@@ -89,10 +84,6 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
8984
// `/:global/api`
9085
fn = generateAPIViewMetadata
9186
break
92-
case 'preview':
93-
// `/:global/preview`
94-
fn = generateLivePreviewViewMetadata
95-
break
9687
case 'versions':
9788
// `/:global/versions`
9889
fn = generateVersionsViewMetadata

packages/next/src/views/Document/index.tsx

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
AdminViewServerProps,
3+
CollectionPreferences,
34
Data,
45
DocumentViewClientProps,
56
DocumentViewServerProps,
@@ -9,7 +10,12 @@ import type {
910
RenderDocumentVersionsProperties,
1011
} from 'payload'
1112

12-
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
13+
import {
14+
DocumentInfoProvider,
15+
EditDepthProvider,
16+
HydrateAuthProvider,
17+
LivePreviewProvider,
18+
} from '@payloadcms/ui'
1319
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
1420
import { isEditing as getIsEditing } from '@payloadcms/ui/shared'
1521
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
@@ -21,6 +27,7 @@ import React from 'react'
2127
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
2228

2329
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
30+
import { getPreferences } from '../../utilities/getPreferences.js'
2431
import { NotFoundView } from '../NotFound/index.js'
2532
import { getDocPreferences } from './getDocPreferences.js'
2633
import { getDocumentData } from './getDocumentData.js'
@@ -84,6 +91,7 @@ export const renderDocument = async ({
8491
payload: {
8592
config,
8693
config: {
94+
admin: { livePreview: livePreviewConfig },
8795
routes: { admin: adminRoute, api: apiRoute },
8896
serverURL,
8997
},
@@ -119,6 +127,7 @@ export const renderDocument = async ({
119127
docPreferences,
120128
{ docPermissions, hasPublishPermission, hasSavePermission },
121129
{ currentEditor, isLocked, lastUpdateTime },
130+
entityPreferences,
122131
] = await Promise.all([
123132
// Get document preferences
124133
getDocPreferences({
@@ -146,8 +155,18 @@ export const renderDocument = async ({
146155
isEditing,
147156
req,
148157
}),
158+
159+
// get entity preferences
160+
getPreferences<CollectionPreferences>(
161+
collectionSlug ? `collection-${collectionSlug}` : `global-${globalSlug}`,
162+
payload,
163+
req.user.id,
164+
req.user.collection,
165+
),
149166
])
150167

168+
const operation = (collectionSlug && idFromArgs) || globalSlug ? 'update' : 'create'
169+
151170
const [
152171
{ hasPublishedDoc, mostRecentVersionIsAutosaved, unpublishedVersionCount, versionCount },
153172
{ state: formState },
@@ -171,7 +190,7 @@ export const renderDocument = async ({
171190
fallbackLocale: false,
172191
globalSlug,
173192
locale: locale?.code,
174-
operation: (collectionSlug && idFromArgs) || globalSlug ? 'update' : 'create',
193+
operation,
175194
renderAllFields: true,
176195
req,
177196
schemaPath: collectionSlug || globalSlug,
@@ -310,6 +329,22 @@ export const renderDocument = async ({
310329
viewType,
311330
}
312331

332+
const livePreviewURL =
333+
typeof livePreviewConfig?.url === 'function'
334+
? await livePreviewConfig.url({
335+
collectionConfig,
336+
data: doc,
337+
globalConfig,
338+
locale,
339+
req,
340+
/**
341+
* @deprecated
342+
* Use `req.payload` instead. This will be removed in the next major version.
343+
*/
344+
payload: initPageResult.req.payload,
345+
})
346+
: livePreviewConfig?.url
347+
313348
return {
314349
data: doc,
315350
Document: (
@@ -337,24 +372,31 @@ export const renderDocument = async ({
337372
unpublishedVersionCount={unpublishedVersionCount}
338373
versionCount={versionCount}
339374
>
340-
{showHeader && !drawerSlug && (
341-
<DocumentHeader
342-
collectionConfig={collectionConfig}
343-
globalConfig={globalConfig}
344-
i18n={i18n}
345-
payload={payload}
346-
permissions={permissions}
347-
/>
348-
)}
349-
<HydrateAuthProvider permissions={permissions} />
350-
<EditDepthProvider>
351-
{RenderServerComponent({
352-
clientProps,
353-
Component: View,
354-
importMap,
355-
serverProps: documentViewServerProps,
356-
})}
357-
</EditDepthProvider>
375+
<LivePreviewProvider
376+
breakpoints={livePreviewConfig?.breakpoints}
377+
isLivePreviewing={entityPreferences?.value?.editViewType === 'live-preview'}
378+
operation={operation}
379+
url={livePreviewURL}
380+
>
381+
{showHeader && !drawerSlug && (
382+
<DocumentHeader
383+
collectionConfig={collectionConfig}
384+
globalConfig={globalConfig}
385+
i18n={i18n}
386+
payload={payload}
387+
permissions={permissions}
388+
/>
389+
)}
390+
<HydrateAuthProvider permissions={permissions} />
391+
<EditDepthProvider>
392+
{RenderServerComponent({
393+
clientProps,
394+
Component: View,
395+
importMap,
396+
serverProps: documentViewServerProps,
397+
})}
398+
</EditDepthProvider>
399+
</LivePreviewProvider>
358400
</DocumentInfoProvider>
359401
),
360402
}

0 commit comments

Comments
 (0)