diff --git a/package.json b/package.json index d1543f8225..79bfe7d771 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@testing-library/react": "13.4.0", "@types/jest": "29.5.4", "@types/node": "20.5.7", + "@types/qs": "6.9.7", "@types/react": "18.2.15", "@types/testing-library__jest-dom": "5.14.8", "copyfiles": "2.4.1", @@ -64,6 +65,7 @@ "qs": "6.11.2", "rimraf": "3.0.2", "shelljs": "0.8.5", + "slate": "0.91.4", "ts-node": "10.9.1", "turbo": "^1.10.15", "typescript": "5.2.2", diff --git a/packages/payload/src/admin/components/views/LivePreview/Preview/index.scss b/packages/payload/src/admin/components/views/LivePreview/Preview/index.scss index a2c588110e..1053184380 100644 --- a/packages/payload/src/admin/components/views/LivePreview/Preview/index.scss +++ b/packages/payload/src/admin/components/views/LivePreview/Preview/index.scss @@ -2,6 +2,8 @@ .live-preview-window { width: 60%; + flex-shrink: 0; + flex-grow: 0; position: sticky; top: var(--doc-controls-height); height: calc(100vh - var(--doc-controls-height)); diff --git a/packages/payload/src/admin/components/views/LivePreview/index.scss b/packages/payload/src/admin/components/views/LivePreview/index.scss index f812579549..f9e425d191 100644 --- a/packages/payload/src/admin/components/views/LivePreview/index.scss +++ b/packages/payload/src/admin/components/views/LivePreview/index.scss @@ -17,13 +17,16 @@ } &__main { - flex-grow: 1; - flex-shrink: 1; + width: 40%; display: flex; flex-direction: column; min-height: 100%; position: relative; + &--popup-open { + width: 100%; + } + &::after { content: ' '; position: absolute; @@ -54,6 +57,7 @@ &__main { min-height: initial; + width: 100%; } &__form { diff --git a/packages/payload/src/admin/components/views/LivePreview/index.tsx b/packages/payload/src/admin/components/views/LivePreview/index.tsx index 0138c5d890..4b528892c8 100644 --- a/packages/payload/src/admin/components/views/LivePreview/index.tsx +++ b/packages/payload/src/admin/components/views/LivePreview/index.tsx @@ -89,7 +89,14 @@ export const LivePreviewView: React.FC = (props) => { .filter(Boolean) .join(' ')} > -
+
siblingData.populateBy === 'collection', + }, + options: [ + { + label: 'Posts', + value: 'posts', + }, + ], + }, + { + type: 'relationship', + name: 'categories', + label: 'Categories To Show', + relationTo: 'categories', + hasMany: true, + admin: { + condition: (_, siblingData) => siblingData.populateBy === 'collection', + }, + }, + { + type: 'number', + name: 'limit', + label: 'Limit', + defaultValue: 10, + admin: { + condition: (_, siblingData) => siblingData.populateBy === 'collection', + step: 1, + }, + }, + { + type: 'relationship', + name: 'selectedDocs', + label: 'Selection', + relationTo: ['posts'], + hasMany: true, + admin: { + condition: (_, siblingData) => siblingData.populateBy === 'selection', + }, + }, + { + type: 'relationship', + name: 'populatedDocs', + label: 'Populated Docs', + relationTo: ['posts'], + hasMany: true, + admin: { + disabled: true, + description: 'This field is auto-populated after-read', + condition: (_, siblingData) => siblingData.populateBy === 'collection', + }, + }, + { + type: 'number', + name: 'populatedDocsTotal', + label: 'Populated Docs Total', + admin: { + step: 1, + disabled: true, + description: 'This field is auto-populated after-read', + condition: (_, siblingData) => siblingData.populateBy === 'collection', + }, + }, + ], +} diff --git a/test/live-preview/blocks/CallToAction/index.ts b/test/live-preview/blocks/CallToAction/index.ts new file mode 100644 index 0000000000..9e8e3c578b --- /dev/null +++ b/test/live-preview/blocks/CallToAction/index.ts @@ -0,0 +1,26 @@ +import type { Block } from '../../../../packages/payload/src/fields/config/types' + +import { invertBackground } from '../../fields/invertBackground' +import linkGroup from '../../fields/linkGroup' + +export const CallToAction: Block = { + slug: 'cta', + labels: { + singular: 'Call to Action', + plural: 'Calls to Action', + }, + fields: [ + invertBackground, + { + name: 'richText', + label: 'Rich Text', + type: 'richText', + }, + linkGroup({ + appearances: ['primary', 'secondary'], + overrides: { + maxRows: 2, + }, + }), + ], +} diff --git a/test/live-preview/blocks/Content/index.ts b/test/live-preview/blocks/Content/index.ts new file mode 100644 index 0000000000..b8832d8e10 --- /dev/null +++ b/test/live-preview/blocks/Content/index.ts @@ -0,0 +1,58 @@ +import type { Block, Field } from '../../../../packages/payload/src/fields/config/types' + +import { invertBackground } from '../../fields/invertBackground' +import link from '../../fields/link' + +const columnFields: Field[] = [ + { + name: 'size', + type: 'select', + defaultValue: 'oneThird', + options: [ + { + value: 'oneThird', + label: 'One Third', + }, + { + value: 'half', + label: 'Half', + }, + { + value: 'twoThirds', + label: 'Two Thirds', + }, + { + value: 'full', + label: 'Full', + }, + ], + }, + { + name: 'richText', + label: 'Rich Text', + type: 'richText', + }, + { + name: 'enableLink', + type: 'checkbox', + }, + link({ + overrides: { + admin: { + condition: (_, { enableLink }) => Boolean(enableLink), + }, + }, + }), +] + +export const Content: Block = { + slug: 'content', + fields: [ + invertBackground, + { + name: 'columns', + type: 'array', + fields: columnFields, + }, + ], +} diff --git a/test/live-preview/blocks/MediaBlock/index.ts b/test/live-preview/blocks/MediaBlock/index.ts new file mode 100644 index 0000000000..2443bbe781 --- /dev/null +++ b/test/live-preview/blocks/MediaBlock/index.ts @@ -0,0 +1,31 @@ +import type { Block } from 'payload/types' + +import { invertBackground } from '../../fields/invertBackground' + +export const MediaBlock: Block = { + slug: 'mediaBlock', + fields: [ + invertBackground, + { + name: 'position', + type: 'select', + defaultValue: 'default', + options: [ + { + label: 'Default', + value: 'default', + }, + { + label: 'Fullscreen', + value: 'fullscreen', + }, + ], + }, + { + name: 'media', + type: 'upload', + relationTo: 'media', + required: true, + }, + ], +} diff --git a/test/live-preview/collections/Categories.ts b/test/live-preview/collections/Categories.ts new file mode 100644 index 0000000000..c2cc38b13f --- /dev/null +++ b/test/live-preview/collections/Categories.ts @@ -0,0 +1,19 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +const Categories: CollectionConfig = { + slug: 'categories', + admin: { + useAsTitle: 'title', + }, + access: { + read: () => true, + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} + +export default Categories diff --git a/test/live-preview/collections/Media.ts b/test/live-preview/collections/Media.ts new file mode 100644 index 0000000000..0113927507 --- /dev/null +++ b/test/live-preview/collections/Media.ts @@ -0,0 +1,20 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +export const Media: CollectionConfig = { + slug: 'media', + upload: true, + access: { + read: () => true, + }, + fields: [ + { + name: 'alt', + type: 'text', + required: true, + }, + { + name: 'caption', + type: 'richText', + }, + ], +} diff --git a/test/live-preview/collections/Pages.ts b/test/live-preview/collections/Pages.ts new file mode 100644 index 0000000000..4b21de512c --- /dev/null +++ b/test/live-preview/collections/Pages.ts @@ -0,0 +1,94 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +import { Archive } from '../blocks/ArchiveBlock' +import { CallToAction } from '../blocks/CallToAction' +import { Content } from '../blocks/Content' +import { MediaBlock } from '../blocks/MediaBlock' +import { hero } from '../fields/hero' + +export const pagesSlug = 'pages' + +export const Pages: CollectionConfig = { + slug: pagesSlug, + access: { + read: () => true, + create: () => true, + update: () => true, + delete: () => true, + }, + admin: { + livePreview: { + url: 'http://localhost:3001', + breakpoints: [ + { + label: 'Mobile', + name: 'mobile', + width: 375, + height: 667, + }, + // { + // label: 'Desktop', + // name: 'desktop', + // width: 1440, + // height: 900, + // }, + ], + }, + useAsTitle: 'title', + defaultColumns: ['id', 'title', 'slug', 'createdAt'], + }, + fields: [ + { + name: 'slug', + type: 'text', + required: true, + admin: { + position: 'sidebar', + }, + }, + { + name: 'title', + type: 'text', + required: true, + }, + { + type: 'tabs', + tabs: [ + { + label: 'Hero', + fields: [hero], + }, + { + label: 'Content', + fields: [ + { + name: 'layout', + type: 'blocks', + required: true, + 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/collections/Posts.ts b/test/live-preview/collections/Posts.ts new file mode 100644 index 0000000000..f0bdc0011d --- /dev/null +++ b/test/live-preview/collections/Posts.ts @@ -0,0 +1,107 @@ +import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' + +import { Archive } from '../blocks/ArchiveBlock' +import { CallToAction } from '../blocks/CallToAction' +import { Content } from '../blocks/Content' +import { MediaBlock } from '../blocks/MediaBlock' +import { hero } from '../fields/hero' + +export const postsSlug = 'posts' + +export const Posts: CollectionConfig = { + slug: postsSlug, + access: { + read: () => true, + create: () => true, + update: () => true, + delete: () => true, + }, + admin: { + livePreview: { + url: 'http://localhost:3001', + breakpoints: [ + { + label: 'Mobile', + name: 'mobile', + width: 375, + height: 667, + }, + // { + // label: 'Desktop', + // name: 'desktop', + // width: 1440, + // height: 900, + // }, + ], + }, + useAsTitle: 'title', + defaultColumns: ['id', 'title', 'slug', 'createdAt'], + }, + fields: [ + { + name: 'slug', + type: 'text', + required: true, + admin: { + position: 'sidebar', + }, + }, + { + name: 'title', + type: 'text', + required: true, + }, + { + type: 'tabs', + tabs: [ + { + label: 'Hero', + fields: [hero], + }, + { + label: 'Content', + fields: [ + { + name: 'layout', + type: 'blocks', + required: true, + blocks: [CallToAction, Content, MediaBlock, Archive], + }, + { + name: 'relatedPosts', + type: 'relationship', + relationTo: 'posts', + hasMany: true, + filterOptions: ({ id }) => { + return { + id: { + not_in: [id], + }, + } + }, + }, + ], + }, + ], + }, + { + 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 ad887f9f96..c464e8ec7a 100644 --- a/test/live-preview/config.ts +++ b/test/live-preview/config.ts @@ -1,15 +1,23 @@ +import path from 'path' + import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import { devUser } from '../credentials' +import Categories from './collections/Categories' +import { Media } from './collections/Media' +import { Pages } from './collections/Pages' +import { Posts, postsSlug } from './collections/Posts' +import { Footer } from './globals/Footer' +import { Header } from './globals/Header' +import { footer } from './seed/footer' +import { header } from './seed/header' +import { home } from './seed/home' +import { post1 } from './seed/post-1' +import { post2 } from './seed/post-2' +import { post3 } from './seed/post-3' +import { postsPage } from './seed/posts-page' -export interface Post { - createdAt: Date - description: string - id: string - title: string - updatedAt: Date -} +export const pagesSlug = 'pages' -export const slug = 'pages' export default buildConfigWithDefaults({ admin: {}, cors: ['http://localhost:3001'], @@ -23,105 +31,12 @@ export default buildConfigWithDefaults({ }, fields: [], }, - { - slug, - access: { - read: () => true, - create: () => true, - update: () => true, - delete: () => true, - }, - admin: { - livePreview: { - url: 'http://localhost:3001', - breakpoints: [ - { - label: 'Mobile', - name: 'mobile', - width: 375, - height: 667, - }, - // { - // label: 'Desktop', - // name: 'desktop', - // width: 1440, - // height: 900, - // }, - ], - }, - }, - fields: [ - { - name: 'title', - type: 'text', - required: true, - }, - { - name: 'description', - type: 'textarea', - required: true, - }, - { - name: 'slug', - type: 'text', - required: true, - admin: { - position: 'sidebar', - }, - }, - { - name: 'layout', - type: 'blocks', - blocks: [ - { - slug: 'hero', - labels: { - singular: 'Hero', - plural: 'Hero', - }, - fields: [ - { - name: 'title', - type: 'text', - required: true, - }, - { - name: 'description', - type: 'textarea', - required: true, - }, - ], - }, - ], - }, - { - name: 'featuredPosts', - type: 'relationship', - relationTo: 'posts', - hasMany: true, - }, - ], - }, - { - slug: 'posts', - access: { - read: () => true, - create: () => true, - update: () => true, - delete: () => true, - }, - admin: { - useAsTitle: 'title', - }, - fields: [ - { - name: 'title', - type: 'text', - required: true, - }, - ], - }, + Pages, + Posts, + Categories, + Media, ], + globals: [Header, Footer], onInit: async (payload) => { await payload.create({ collection: 'users', @@ -131,28 +46,54 @@ export default buildConfigWithDefaults({ }, }) - const post1 = await payload.create({ - collection: 'posts', + const media = await payload.create({ + collection: 'media', + filePath: path.resolve(__dirname, 'image-1.jpg'), data: { - title: 'Post 1', + alt: 'Image 1', }, }) + const [post1Doc, post2Doc, post3Doc] = await Promise.all([ + await payload.create({ + collection: postsSlug, + data: JSON.parse(JSON.stringify(post1).replace(/\{\{IMAGE\}\}/g, media.id)), + }), + await payload.create({ + collection: postsSlug, + data: JSON.parse(JSON.stringify(post2).replace(/\{\{IMAGE\}\}/g, media.id)), + }), + await payload.create({ + collection: postsSlug, + data: JSON.parse(JSON.stringify(post3).replace(/\{\{IMAGE\}\}/g, media.id)), + }), + ]) + + const postsPageDoc = await payload.create({ + collection: pagesSlug, + data: JSON.parse(JSON.stringify(postsPage).replace(/\{\{IMAGE\}\}/g, media.id)), + }) + await payload.create({ - collection: slug, - data: { - title: 'Hello, world!', - description: 'This is an example of live preview.', - slug: 'home', - layout: [ - { - blockType: 'hero', - title: 'Hello, world!', - description: 'This is an example of live preview.', - }, - ], - featuredPosts: [post1.id], - }, + collection: pagesSlug, + data: JSON.parse( + JSON.stringify(home) + .replace(/\{\{MEDIA_ID\}\}/g, media.id) + .replace(/\{\{POSTS_PAGE_ID\}\}/g, postsPageDoc.id) + .replace(/\{\{POST_1_ID\}\}/g, post1Doc.id) + .replace(/\{\{POST_2_ID\}\}/g, post2Doc.id) + .replace(/\{\{POST_3_ID\}\}/g, post3Doc.id), + ), + }) + + await payload.updateGlobal({ + slug: 'header', + data: JSON.parse(JSON.stringify(header).replace(/\{\{POSTS_PAGE_ID\}\}/g, postsPageDoc.id)), + }) + + await payload.updateGlobal({ + slug: 'footer', + data: JSON.parse(JSON.stringify(footer)), }) }, }) diff --git a/test/live-preview/fields/hero.ts b/test/live-preview/fields/hero.ts new file mode 100644 index 0000000000..c85f005557 --- /dev/null +++ b/test/live-preview/fields/hero.ts @@ -0,0 +1,44 @@ +import type { Field } from '../../../packages/payload/src/fields/config/types' + +export const hero: Field = { + name: 'hero', + label: false, + type: 'group', + fields: [ + { + type: 'select', + name: 'type', + label: 'Type', + required: true, + defaultValue: 'lowImpact', + options: [ + { + label: 'None', + value: 'none', + }, + { + label: 'High Impact', + value: 'highImpact', + }, + { + label: 'Low Impact', + value: 'lowImpact', + }, + ], + }, + { + name: 'richText', + label: 'Rich Text', + type: 'richText', + }, + { + name: 'media', + type: 'upload', + relationTo: 'media', + required: true, + admin: { + condition: (_, { type } = {}) => ['highImpact'].includes(type), + }, + }, + ], +} diff --git a/test/live-preview/fields/invertBackground.ts b/test/live-preview/fields/invertBackground.ts new file mode 100644 index 0000000000..76d565fff0 --- /dev/null +++ b/test/live-preview/fields/invertBackground.ts @@ -0,0 +1,6 @@ +import type { CheckboxField } from '../../../packages/payload/src/fields/config/types' + +export const invertBackground: CheckboxField = { + name: 'invertBackground', + type: 'checkbox', +} diff --git a/test/live-preview/fields/link.ts b/test/live-preview/fields/link.ts new file mode 100644 index 0000000000..a1003842ea --- /dev/null +++ b/test/live-preview/fields/link.ts @@ -0,0 +1,150 @@ +import type { Field } from '../../../packages/payload/src/fields/config/types' + +import deepMerge from '../utilities/deepMerge' + +export const appearanceOptions = { + primary: { + label: 'Primary Button', + value: 'primary', + }, + secondary: { + label: 'Secondary Button', + value: 'secondary', + }, + default: { + label: 'Default', + value: 'default', + }, +} + +export type LinkAppearances = 'default' | 'primary' | 'secondary' + +type LinkType = (options?: { + appearances?: LinkAppearances[] | false + disableLabel?: boolean + overrides?: Record +}) => Field + +const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = {}) => { + const linkResult: Field = { + name: 'link', + type: 'group', + admin: { + hideGutter: true, + }, + fields: [ + { + type: 'row', + fields: [ + { + name: 'type', + type: 'radio', + options: [ + { + label: 'Internal link', + value: 'reference', + }, + { + label: 'Custom URL', + value: 'custom', + }, + ], + defaultValue: 'reference', + admin: { + layout: 'horizontal', + width: '50%', + }, + }, + { + name: 'newTab', + label: 'Open in new tab', + type: 'checkbox', + admin: { + width: '50%', + style: { + alignSelf: 'flex-end', + }, + }, + }, + ], + }, + ], + } + + const linkTypes: Field[] = [ + { + name: 'reference', + label: 'Document to link to', + type: 'relationship', + relationTo: ['pages'], + required: true, + maxDepth: 1, + admin: { + condition: (_, siblingData) => siblingData?.type === 'reference', + }, + }, + { + name: 'url', + label: 'Custom URL', + type: 'text', + required: true, + admin: { + condition: (_, siblingData) => siblingData?.type === 'custom', + }, + }, + ] + + if (!disableLabel) { + linkTypes.map((linkType) => ({ + ...linkType, + admin: { + ...linkType.admin, + width: '50%', + }, + })) + + linkResult.fields.push({ + type: 'row', + fields: [ + ...linkTypes, + { + name: 'label', + label: 'Label', + type: 'text', + required: true, + admin: { + width: '50%', + }, + }, + ], + }) + } else { + linkResult.fields = [...linkResult.fields, ...linkTypes] + } + + if (appearances !== false) { + let appearanceOptionsToUse = [ + appearanceOptions.default, + appearanceOptions.primary, + appearanceOptions.secondary, + ] + + if (appearances) { + appearanceOptionsToUse = appearances.map((appearance) => appearanceOptions[appearance]) + } + + linkResult.fields.push({ + name: 'appearance', + type: 'select', + defaultValue: 'default', + options: appearanceOptionsToUse, + admin: { + description: 'Choose how the link should be rendered.', + }, + }) + } + + return deepMerge(linkResult, overrides) +} + +export default link diff --git a/test/live-preview/fields/linkGroup.ts b/test/live-preview/fields/linkGroup.ts new file mode 100644 index 0000000000..86df275024 --- /dev/null +++ b/test/live-preview/fields/linkGroup.ts @@ -0,0 +1,26 @@ +import type { ArrayField, Field } from '../../../packages/payload/src/fields/config/types' +import type { LinkAppearances } from './link' + +import deepMerge from '../utilities/deepMerge' +import link from './link' + +type LinkGroupType = (options?: { + appearances?: LinkAppearances[] | false + overrides?: Partial +}) => Field + +const linkGroup: LinkGroupType = ({ overrides = {}, appearances } = {}) => { + const generatedLinkGroup: Field = { + name: 'links', + type: 'array', + fields: [ + link({ + appearances, + }), + ], + } + + return deepMerge(generatedLinkGroup, overrides) +} + +export default linkGroup diff --git a/test/live-preview/globals/Footer.ts b/test/live-preview/globals/Footer.ts new file mode 100644 index 0000000000..a8fcaf6ae9 --- /dev/null +++ b/test/live-preview/globals/Footer.ts @@ -0,0 +1,18 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +import link from '../fields/link' + +export const Footer: GlobalConfig = { + slug: 'footer', + access: { + read: () => true, + }, + fields: [ + { + name: 'navItems', + type: 'array', + maxRows: 6, + fields: [link()], + }, + ], +} diff --git a/test/live-preview/globals/Header.ts b/test/live-preview/globals/Header.ts new file mode 100644 index 0000000000..74c7fb53fc --- /dev/null +++ b/test/live-preview/globals/Header.ts @@ -0,0 +1,18 @@ +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +import link from '../fields/link' + +export const Header: GlobalConfig = { + slug: 'header', + access: { + read: () => true, + }, + fields: [ + { + name: 'navItems', + type: 'array', + maxRows: 6, + fields: [link()], + }, + ], +} diff --git a/test/live-preview/image-1.jpg b/test/live-preview/image-1.jpg new file mode 100644 index 0000000000..a1c68b7a24 Binary files /dev/null and b/test/live-preview/image-1.jpg differ diff --git a/test/live-preview/int.spec.ts b/test/live-preview/int.spec.ts new file mode 100644 index 0000000000..f32e2be494 --- /dev/null +++ b/test/live-preview/int.spec.ts @@ -0,0 +1,54 @@ +import type { Page } from './payload-types' + +import payload from '../../packages/payload/src' +import { mergeLivePreviewData } from '../../packages/payload/src/admin/components/views/LivePreview/useLivePreview/mergeData' +import { fieldSchemaToJSON } from '../../packages/payload/src/utilities/fieldSchemaToJSON' +import { initPayloadTest } from '../helpers/configHelpers' +import { RESTClient } from '../helpers/rest' +import { Pages } from './collections/Pages' +import configPromise, { pagesSlug } from './config' + +require('isomorphic-fetch') + +let client +let serverURL + +describe('Collections - Live Preview', () => { + beforeAll(async () => { + const { serverURL: incomingServerURL } = await initPayloadTest({ + __dirname, + init: { local: false }, + }) + + serverURL = incomingServerURL + const config = await configPromise + client = new RESTClient(config, { serverURL, defaultSlug: pagesSlug }) + await client.login() + }) + + it('merges live preview data', async () => { + const testPage = await payload.create({ + collection: pagesSlug, + data: { + slug: 'home', + title: 'Test Page', + }, + }) + + expect(testPage?.id).toBeDefined() + + const pageEdits: Page = { + title: 'Test Page (Changed)', + } as Page + + const mergedData = await mergeLivePreviewData({ + depth: 1, + existingData: testPage, + fieldSchema: fieldSchemaToJSON(Pages.fields), + incomingData: pageEdits, + serverURL, + }) + + expect(mergedData.title).toEqual(pageEdits.title) + }) +}) diff --git a/test/live-preview/next-app/app/(pages)/[slug]/page.client.tsx b/test/live-preview/next-app/app/(pages)/[slug]/page.client.tsx new file mode 100644 index 0000000000..b01d8fbac9 --- /dev/null +++ b/test/live-preview/next-app/app/(pages)/[slug]/page.client.tsx @@ -0,0 +1,29 @@ +'use client' + +import { Page as PageType } from '@/payload-types' +import { useLivePreview } from '../../../../../../packages/live-preview-react' +import React from 'react' +import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL' +import { Hero } from '@/app/_components/Hero' +import { Blocks } from '@/app/_components/Blocks' + +export const PageClient: React.FC<{ + page: PageType +}> = ({ page: initialPage }) => { + const { data } = useLivePreview({ + initialData: initialPage, + serverURL: PAYLOAD_SERVER_URL, + }) + + return ( + + + + + ) +} diff --git a/test/live-preview/next-app/app/(pages)/[slug]/page.tsx b/test/live-preview/next-app/app/(pages)/[slug]/page.tsx new file mode 100644 index 0000000000..d655ef5685 --- /dev/null +++ b/test/live-preview/next-app/app/(pages)/[slug]/page.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { notFound } from 'next/navigation' + +import { Page } from '../../../payload-types' +import { fetchDocs } from '@/app/_api/fetchDocs' +import { fetchDoc } from '@/app/_api/fetchDoc' +import { PageClient } from './page.client' + +export default async function Page({ params: { slug = 'home' } }) { + let page: Page | null = null + + try { + page = await fetchDoc({ + collection: 'pages', + slug, + }) + } catch (error) { + console.error(error) + } + + if (!page) { + return notFound() + } + + return +} + +export async function generateStaticParams() { + try { + const pages = await fetchDocs('pages') + return pages?.map(({ slug }) => slug) + } catch (error) { + return [] + } +} diff --git a/test/live-preview/next-app/app/(pages)/posts/[slug]/page.tsx b/test/live-preview/next-app/app/(pages)/posts/[slug]/page.tsx new file mode 100644 index 0000000000..250a432349 --- /dev/null +++ b/test/live-preview/next-app/app/(pages)/posts/[slug]/page.tsx @@ -0,0 +1,92 @@ +import React from 'react' +import { notFound } from 'next/navigation' + +import { Post } from '../../../../payload-types' +import { fetchDoc } from '../../../_api/fetchDoc' +import { fetchDocs } from '../../../_api/fetchDocs' +import { Blocks } from '../../../_components/Blocks' +import { PostHero } from '../../../_heros/PostHero' + +export default async function Post(args: { + params: { + slug: string + } +}) { + const { + params: { slug = 'home' }, + } = args + + let post: Post | null = null + + try { + post = await fetchDoc({ + collection: 'posts', + slug, + }) + } catch (error) { + console.error(error) // eslint-disable-line no-console + } + + if (!post) { + notFound() + } + + const { layout, relatedPosts } = post + + return ( + + + + + + ) +} + +export async function generateStaticParams() { + try { + const posts = await fetchDocs('posts') + return posts?.map(({ slug }) => slug) + } catch (error) { + return [] + } +} diff --git a/test/live-preview/next-app/app/_api/fetchDoc.ts b/test/live-preview/next-app/app/_api/fetchDoc.ts new file mode 100644 index 0000000000..62cc1c4830 --- /dev/null +++ b/test/live-preview/next-app/app/_api/fetchDoc.ts @@ -0,0 +1,30 @@ +import type { Config } from '../../payload-types' +import { PAYLOAD_SERVER_URL } from './serverURL' + +export const fetchDoc = async (args: { + collection: keyof Config['collections'] + slug?: string + id?: string +}): Promise => { + const { collection, slug, id } = args || {} + + const doc: T = await fetch( + `${PAYLOAD_SERVER_URL}/api/${collection}${id ? `/${id}` : ''}${ + slug ? `?where[slug][equals]=${slug}` : '' + }`, + { + method: 'GET', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + ?.then((res) => res.json()) + ?.then((res) => { + if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc') + return res?.docs?.[0] + }) + + return doc +} diff --git a/test/live-preview/next-app/app/_api/fetchDocs.ts b/test/live-preview/next-app/app/_api/fetchDocs.ts new file mode 100644 index 0000000000..62e13398b4 --- /dev/null +++ b/test/live-preview/next-app/app/_api/fetchDocs.ts @@ -0,0 +1,20 @@ +import type { Config } from '../../payload-types' +import { PAYLOAD_SERVER_URL } from './serverURL' + +export const fetchDocs = async (collection: keyof Config['collections']): Promise => { + const docs: T[] = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}?limit=100`, { + method: 'GET', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + }) + ?.then((res) => res.json()) + ?.then((res) => { + if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching docs') + + return res?.docs + }) + + return docs +} diff --git a/test/live-preview/next-app/app/_api/fetchFooter.ts b/test/live-preview/next-app/app/_api/fetchFooter.ts new file mode 100644 index 0000000000..a5144b66ea --- /dev/null +++ b/test/live-preview/next-app/app/_api/fetchFooter.ts @@ -0,0 +1,24 @@ +import type { Footer } from '../../payload-types' +import { PAYLOAD_SERVER_URL } from './serverURL' + +export async function fetchFooter(): Promise