Compare commits

...

7 Commits

Author SHA1 Message Date
Jessica Chowdhury
083cee37aa fix: adds next/auth to publish config export 2025-04-17 14:56:24 +01:00
Jessica Chowdhury
b0fa1e9768 Merge branch 'main' into feat/live-preview-tab-default 2025-04-17 12:35:54 +01:00
Jessica Chowdhury
fcc4b22715 docs: adds livePreview.defaultTab option to docs 2025-04-17 12:35:28 +01:00
Jessica Chowdhury
8abeca5568 chore: add e2e tests for livePreview.defaultTab 2025-04-17 12:19:42 +01:00
Jessica Chowdhury
68a3ae80f2 chore: abstracts live preview conditional logic 2025-04-17 11:19:40 +01:00
Jessica Chowdhury
d51c6d4f52 feat(next): adds option livePreview.defaultTab 2025-04-15 17:35:21 +01:00
Jessica Chowdhury
313bfff781 feat(next): updates default doc tab to live preview when enabled 2025-04-15 11:45:02 +01:00
15 changed files with 307 additions and 30 deletions

View File

@@ -43,12 +43,13 @@ Setting up Live Preview is easy. This can be done either globally through the [R
The following options are available:
| Path | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`url`** \* | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). |
| **`breakpoints`** | Array of breakpoints to be used as “device sizes” in the preview window. Each item appears as an option in the toolbar. [More details](#breakpoints). |
| **`collections`** | Array of collection slugs to enable Live Preview on. |
| **`globals`** | Array of global slugs to enable Live Preview on. |
| Path | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`url`** \* | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). |
| **`breakpoints`** | Array of breakpoints to be used as “device sizes” in the preview window. Each item appears as an option in the toolbar. [More details](#breakpoints). |
| **`collections`** | Array of collection slugs to enable Live Preview on. |
| **`globals`** | Array of global slugs to enable Live Preview on. |
| **`defaultTab`** | Boolean to set Live Preview as the default tab when editing documents. Applies to all collections and globals where Live Preview is enabled. Defaults to `false`. |
_\* An asterisk denotes that a property is required._

View File

@@ -156,6 +156,11 @@
"types": "./dist/exports/templates.d.ts",
"default": "./dist/exports/templates.js"
},
"./auth": {
"import": "./dist/exports/auth.js",
"types": "./dist/exports/auth.d.ts",
"default": "./dist/exports/auth.js"
},
"./utilities": {
"import": "./dist/exports/utilities.js",
"types": "./dist/exports/utilities.d.ts",

View File

@@ -44,7 +44,7 @@ export const DocumentTabLink: React.FC<{
docPath += `/${segmentThree}`
}
const href = `${docPath}${hrefFromProps}`
const href = `${docPath}${hrefFromProps}`.replace(/\/$/, '')
// separated the two so it doesn't break checks against pathname
const hrefWithLocale = `${href}${locale ? `?locale=${locale}` : ''}`

View File

@@ -1,6 +1,7 @@
import type { DocumentTabConfig } from 'payload'
import type React from 'react'
import { isLivePreviewEnabled } from './isLivePreviewEnabled.js'
import { VersionsPill } from './VersionsPill/index.js'
export const documentViewKeys = [
@@ -31,34 +32,39 @@ export const tabs: Record<
order: 1000,
},
default: {
condition: ({ collectionConfig, config, globalConfig }) => {
return !isLivePreviewEnabled({ collectionConfig, config, globalConfig })
},
href: '',
// isActive: ({ href, location }) =>
// location.pathname === href || location.pathname === `${href}/create`,
label: ({ t }) => t('general:edit'),
order: 0,
},
edit: {
condition: ({ collectionConfig, config, globalConfig }) => {
return isLivePreviewEnabled({ collectionConfig, config, globalConfig })
},
href: '/edit',
label: ({ t }) => t('general:edit'),
order: 200,
},
livePreview: {
condition: ({ collectionConfig, config, globalConfig }) => {
if (collectionConfig) {
return Boolean(
config?.admin?.livePreview?.collections?.includes(collectionConfig.slug) ||
collectionConfig?.admin?.livePreview,
)
}
if (globalConfig) {
return Boolean(
config?.admin?.livePreview?.globals?.includes(globalConfig.slug) ||
globalConfig?.admin?.livePreview,
)
}
return false
return !isLivePreviewEnabled({ collectionConfig, config, globalConfig })
},
href: '/preview',
label: ({ t }) => t('general:livePreview'),
order: 100,
},
livePreviewDefault: {
condition: ({ collectionConfig, config, globalConfig }) => {
return isLivePreviewEnabled({ collectionConfig, config, globalConfig })
},
href: '',
label: ({ t }) => t('general:livePreview'),
order: 0,
},
references: {
condition: () => false,
},
@@ -77,7 +83,7 @@ export const tabs: Record<
),
href: '/versions',
label: ({ t }) => t('version:versions'),
order: 200,
order: 300,
Pill_Component: VersionsPill,
},
}

View File

@@ -0,0 +1,29 @@
import type { CollectionConfig, Config, GlobalConfig, SanitizedConfig } from 'payload'
export function isLivePreviewEnabled({
collectionConfig,
config,
globalConfig,
}: {
collectionConfig?: CollectionConfig
config?: Config | SanitizedConfig
globalConfig?: GlobalConfig
}): boolean {
let isLivePreview = false
if (collectionConfig) {
isLivePreview = Boolean(
config?.admin?.livePreview?.collections?.includes(collectionConfig.slug) ||
collectionConfig?.admin?.livePreview,
)
}
if (globalConfig) {
isLivePreview = Boolean(
config?.admin?.livePreview?.globals?.includes(globalConfig.slug) ||
globalConfig?.admin?.livePreview,
)
}
return Boolean(config?.admin?.livePreview?.defaultTab && isLivePreview)
}

View File

@@ -74,6 +74,8 @@ export const getViewsFromConfig = ({
(globalConfig && globalConfig?.admin?.livePreview) ||
config?.admin?.livePreview?.globals?.includes(globalConfig?.slug)
const useLivePreviewAsDefault = config?.admin?.livePreview?.defaultTab && livePreviewEnabled
if (collectionConfig) {
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
routeSegments
@@ -130,11 +132,14 @@ export const getViewsFromConfig = ({
}
} else {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'default'),
ComponentConfig: getCustomViewByKey(
views,
useLivePreviewAsDefault ? 'live-preview' : 'default',
),
}
DefaultView = {
Component: DefaultEditView,
Component: useLivePreviewAsDefault ? DefaultLivePreviewView : DefaultEditView,
}
}
@@ -159,8 +164,21 @@ export const getViewsFromConfig = ({
break
}
case 'edit': {
if (useLivePreviewAsDefault) {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'default'),
}
DefaultView = {
Component: DefaultEditView,
}
}
break
}
case 'preview': {
if (livePreviewEnabled) {
if (livePreviewEnabled && !useLivePreviewAsDefault) {
DefaultView = {
Component: DefaultLivePreviewView,
}
@@ -283,10 +301,14 @@ export const getViewsFromConfig = ({
switch (routeSegments.length) {
case 2: {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'default'),
ComponentConfig: getCustomViewByKey(
views,
useLivePreviewAsDefault ? 'live-preview' : 'default',
),
}
DefaultView = {
Component: DefaultEditView,
Component: useLivePreviewAsDefault ? DefaultLivePreviewView : DefaultEditView,
}
break
}
@@ -306,8 +328,21 @@ export const getViewsFromConfig = ({
break
}
case 'edit': {
if (useLivePreviewAsDefault) {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'default'),
}
DefaultView = {
Component: DefaultEditView,
}
}
break
}
case 'preview': {
if (livePreviewEnabled) {
if (livePreviewEnabled && !useLivePreviewAsDefault) {
DefaultView = {
Component: DefaultLivePreviewView,
}

View File

@@ -148,6 +148,13 @@ export type LivePreviewConfig = {
name: string
width: number | string
}[]
/**
* Set Live Preview as the default tab when editing documents.
* If enabled, the Live Preview tab will be shown first instead of the Edit tab.
* This applies to all collections or globals where Live Preview is enabled.
* False by default.
*/
defaultTab?: boolean
/**
The URL of the frontend application. This will be rendered within an `iframe` as its `src`.
Payload will send a `window.postMessage()` to this URL with the document data in real-time.

View File

@@ -0,0 +1,13 @@
import type { CollectionConfig } from 'payload'
import { collectionWithLivePreviewSlug } from '../slugs.js'
export const CollectionWithLivePreview: CollectionConfig = {
slug: collectionWithLivePreviewSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
}

View File

@@ -4,6 +4,7 @@ const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { BaseListFilter } from './collections/BaseListFilter.js'
import { CollectionWithLivePreview } from './collections/CollectionWithLivePreview.js'
import { CustomFields } from './collections/CustomFields/index.js'
import { CustomViews1 } from './collections/CustomViews1.js'
import { CustomViews2 } from './collections/CustomViews2.js'
@@ -26,6 +27,7 @@ import { with300Documents } from './collections/With300Documents.js'
import { CustomGlobalViews1 } from './globals/CustomViews1.js'
import { CustomGlobalViews2 } from './globals/CustomViews2.js'
import { Global } from './globals/Global.js'
import { GlobalWithLivePreview } from './globals/GlobalWithLivePreview.js'
import { GlobalGroup1A } from './globals/Group1A.js'
import { GlobalGroup1B } from './globals/Group1B.js'
import { GlobalHidden } from './globals/Hidden.js'
@@ -42,6 +44,7 @@ import {
protectedCustomNestedViewPath,
publicCustomViewPath,
} from './shared.js'
import { collectionWithLivePreviewSlug, globalWithLivePreviewSlug } from './slugs.js'
export default buildConfigWithDefaults({
admin: {
@@ -110,6 +113,12 @@ export default buildConfigWithDefaults({
},
},
},
livePreview: {
collections: [collectionWithLivePreviewSlug],
globals: [globalWithLivePreviewSlug],
url: 'http://localhost:3000',
defaultTab: true,
},
meta: {
description: 'This is a custom meta description',
icons: [
@@ -163,6 +172,7 @@ export default buildConfigWithDefaults({
BaseListFilter,
with300Documents,
ListDrawer,
CollectionWithLivePreview,
],
globals: [
GlobalHidden,
@@ -174,6 +184,7 @@ export default buildConfigWithDefaults({
GlobalGroup1A,
GlobalGroup1B,
Settings,
GlobalWithLivePreview,
],
i18n: {
translations: {

View File

@@ -26,10 +26,12 @@ import {
customTabViewTitle,
} from '../../shared.js'
import {
collectionWithLivePreviewSlug,
customFieldsSlug,
customGlobalViews2GlobalSlug,
customViews2CollectionSlug,
globalSlug,
globalWithLivePreviewSlug,
group1Collection1Slug,
group1GlobalSlug,
noApiViewCollectionSlug,
@@ -64,6 +66,9 @@ describe('Document View', () => {
let serverURL: string
let customViewsURL: AdminUrlUtil
let customFieldsURL: AdminUrlUtil
let collectionWithLivePreviewURL: AdminUrlUtil
let globalWithLivePreviewURL: AdminUrlUtil
let collectionWithLivePreviewDocId: string
beforeAll(async ({ browser }, testInfo) => {
const prebuild = false // Boolean(process.env.CI)
@@ -79,6 +84,8 @@ describe('Document View', () => {
globalURL = new AdminUrlUtil(serverURL, globalSlug)
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
customFieldsURL = new AdminUrlUtil(serverURL, customFieldsSlug)
collectionWithLivePreviewURL = new AdminUrlUtil(serverURL, collectionWithLivePreviewSlug)
globalWithLivePreviewURL = new AdminUrlUtil(serverURL, globalWithLivePreviewSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -186,7 +193,7 @@ describe('Document View', () => {
await page.goto(postsUrl.create)
await page.locator('#field-title')?.fill(title)
await saveDocAndAssert(page)
await wait(500)
await expect(page.locator('.doc-tabs__tabs-container .doc-tab')).toHaveCount(2)
await checkPageTitle(page, title)
await checkBreadcrumb(page, title)
})
@@ -523,6 +530,92 @@ describe('Document View', () => {
await expect(fileField).toHaveValue('some file text')
})
})
describe('tabs when livePreview.default tab is set to true', () => {
beforeAll(async () => {
await page.goto(collectionWithLivePreviewURL.list)
const firstDoc = await page.locator('.collection-list .table a').first().getAttribute('href')
const id = firstDoc?.split('/').pop()
if (id) {
collectionWithLivePreviewDocId = id
}
})
test('collection — should show live preview as first tab and default view', async () => {
await page.goto(collectionWithLivePreviewURL.edit(collectionWithLivePreviewDocId))
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const firstTab = tabs.first()
await expect(firstTab).toContainText('Live Preview')
const iframe = page.locator('.live-preview-iframe')
await expect(iframe).toBeVisible()
})
test('collection — should show edit as second tab', async () => {
await page.goto(collectionWithLivePreviewURL.edit(collectionWithLivePreviewDocId))
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const secondTab = tabs.nth(1)
await expect(secondTab).toContainText('Edit')
})
test('collection — should have `/edit` path', async () => {
await page.goto(collectionWithLivePreviewURL.edit(collectionWithLivePreviewDocId))
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const secondTab = tabs.nth(1)
await secondTab.click()
await expect(page).toHaveURL(/\/edit/)
})
test('collection — should not have `/preview` path', async () => {
await page.goto(collectionWithLivePreviewURL.edit(collectionWithLivePreviewDocId))
await expect(page).not.toHaveURL(/\/preview/)
const previewURL = `${collectionWithLivePreviewURL.edit(collectionWithLivePreviewDocId)}/preview`
await page.goto(previewURL)
const notFound = page.locator('text=Not Found')
await expect(notFound).toBeVisible()
})
test('globals — should show live preview as first tab', async () => {
await page.goto(globalWithLivePreviewURL.global(globalWithLivePreviewSlug))
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const firstTab = tabs.first()
await expect(firstTab).toContainText('Live Preview')
const iframe = page.locator('.live-preview-iframe')
await expect(iframe).toBeVisible()
})
test('globals — should show edit as second tab', async () => {
await page.goto(globalWithLivePreviewURL.global(globalWithLivePreviewSlug))
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const secondTab = tabs.nth(1)
await expect(secondTab).toContainText('Edit')
})
test('globals — should have `/edit` path', async () => {
await page.goto(globalWithLivePreviewURL.global(globalWithLivePreviewSlug))
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const secondTab = tabs.nth(1)
await secondTab.click()
await expect(page).toHaveURL(/\/edit/)
})
test('globals — should not have `/preview` path', async () => {
await page.goto(globalWithLivePreviewURL.global(globalWithLivePreviewSlug))
await expect(page).not.toHaveURL(/\/preview/)
const previewURL = `${globalWithLivePreviewURL.global(globalWithLivePreviewSlug)}/preview`
await page.goto(previewURL)
const notFound = page.locator('text=Not Found')
await expect(notFound).toBeVisible()
})
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {

View File

@@ -0,0 +1,13 @@
import type { GlobalConfig } from 'payload'
import { globalWithLivePreviewSlug } from '../slugs.js'
export const GlobalWithLivePreview: GlobalConfig = {
slug: globalWithLivePreviewSlug,
fields: [
{
type: 'text',
name: 'text',
},
],
}

View File

@@ -87,6 +87,7 @@ export interface Config {
'base-list-filters': BaseListFilter;
with300documents: With300Document;
'with-list-drawer': WithListDrawer;
'collection-with-live-preview': CollectionWithLivePreview;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -113,6 +114,7 @@ export interface Config {
'base-list-filters': BaseListFiltersSelect<false> | BaseListFiltersSelect<true>;
with300documents: With300DocumentsSelect<false> | With300DocumentsSelect<true>;
'with-list-drawer': WithListDrawerSelect<false> | WithListDrawerSelect<true>;
'collection-with-live-preview': CollectionWithLivePreviewSelect<false> | CollectionWithLivePreviewSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -130,6 +132,7 @@ export interface Config {
'group-globals-one': GroupGlobalsOne;
'group-globals-two': GroupGlobalsTwo;
settings: Setting;
'global-with-live-preview': GlobalWithLivePreview;
};
globalsSelect: {
'hidden-global': HiddenGlobalSelect<false> | HiddenGlobalSelect<true>;
@@ -141,6 +144,7 @@ export interface Config {
'group-globals-one': GroupGlobalsOneSelect<false> | GroupGlobalsOneSelect<true>;
'group-globals-two': GroupGlobalsTwoSelect<false> | GroupGlobalsTwoSelect<true>;
settings: SettingsSelect<false> | SettingsSelect<true>;
'global-with-live-preview': GlobalWithLivePreviewSelect<false> | GlobalWithLivePreviewSelect<true>;
};
locale: 'es' | 'en';
user: User & {
@@ -467,6 +471,16 @@ export interface WithListDrawer {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collection-with-live-preview".
*/
export interface CollectionWithLivePreview {
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -553,6 +567,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'with-list-drawer';
value: string | WithListDrawer;
} | null)
| ({
relationTo: 'collection-with-live-preview';
value: string | CollectionWithLivePreview;
} | null);
globalSlug?: string | null;
user: {
@@ -866,6 +884,15 @@ export interface WithListDrawerSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collection-with-live-preview_select".
*/
export interface CollectionWithLivePreviewSelect<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".
@@ -989,6 +1016,16 @@ export interface Setting {
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global-with-live-preview".
*/
export interface GlobalWithLivePreview {
id: string;
text?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hidden-global_select".
@@ -1080,6 +1117,16 @@ export interface SettingsSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global-with-live-preview_select".
*/
export interface GlobalWithLivePreviewSelect<T extends boolean = true> {
text?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".

View File

@@ -5,6 +5,7 @@ import { executePromises } from '../helpers/executePromises.js'
import { seedDB } from '../helpers/seed.js'
import {
collectionSlugs,
collectionWithLivePreviewSlug,
customViews1CollectionSlug,
customViews2CollectionSlug,
geoCollectionSlug,
@@ -115,6 +116,15 @@ export const seed = async (_payload) => {
depth: 0,
overrideAccess: true,
}),
() =>
_payload.create({
collection: collectionWithLivePreviewSlug,
data: {
title: 'Live Preview Default Tab',
},
depth: 0,
overrideAccess: true,
}),
],
false,
)

View File

@@ -18,6 +18,8 @@ export const uploadTwoCollectionSlug = 'uploads-two'
export const customFieldsSlug = 'custom-fields'
export const listDrawerSlug = 'with-list-drawer'
export const collectionWithLivePreviewSlug = 'collection-with-live-preview'
export const collectionSlugs = [
usersCollectionSlug,
customViews1CollectionSlug,
@@ -33,6 +35,7 @@ export const collectionSlugs = [
customFieldsSlug,
disableDuplicateSlug,
listDrawerSlug,
collectionWithLivePreviewSlug,
]
export const customGlobalViews1GlobalSlug = 'custom-global-views-one'
@@ -45,6 +48,8 @@ export const hiddenGlobalSlug = 'hidden-global'
export const notInViewGlobalSlug = 'not-in-view-global'
export const settingsGlobalSlug = 'settings'
export const noApiViewGlobalSlug = 'global-no-api-view'
export const globalWithLivePreviewSlug = 'global-with-live-preview'
export const globalSlugs = [
customGlobalViews1GlobalSlug,
customGlobalViews2GlobalSlug,
@@ -53,5 +58,6 @@ export const globalSlugs = [
group2GlobalSlug,
hiddenGlobalSlug,
noApiViewGlobalSlug,
globalWithLivePreviewSlug,
]
export const with300DocumentsSlug = 'with300documents'

View File

@@ -40,6 +40,7 @@ export default buildConfigWithDefaults({
breakpoints: [mobileBreakpoint, desktopBreakpoint],
collections: [pagesSlug, postsSlug, ssrPagesSlug, ssrAutosavePagesSlug],
globals: ['header', 'footer'],
// defaultTab: true,
},
},
cors: ['http://localhost:3000', 'http://localhost:3001'],