chore: handles live preview data (#3440)
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { LivePreview as LivePreviewType } from '../../../../../exports/config'
|
import type { LivePreview as LivePreviewType } from '../../../../../exports/config'
|
||||||
|
import type { Field } from '../../../../../fields/config/types'
|
||||||
import type { EditViewProps } from '../../types'
|
import type { EditViewProps } from '../../types'
|
||||||
import type { usePopupWindow } from '../usePopupWindow'
|
import type { usePopupWindow } from '../usePopupWindow'
|
||||||
|
|
||||||
|
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
|
||||||
import { useAllFormFields } from '../../../forms/Form/context'
|
import { useAllFormFields } from '../../../forms/Form/context'
|
||||||
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
|
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
|
||||||
import { LivePreviewProvider } from '../Context'
|
import { LivePreviewProvider } from '../Context'
|
||||||
@@ -33,13 +35,31 @@ const Preview: React.FC<
|
|||||||
|
|
||||||
const [fields] = useAllFormFields()
|
const [fields] = useAllFormFields()
|
||||||
|
|
||||||
|
const [fieldSchemaJSON] = useState(() => {
|
||||||
|
let fields: Field[]
|
||||||
|
|
||||||
|
if ('collection' in props) {
|
||||||
|
const { collection } = props
|
||||||
|
fields = collection.fields
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('global' in props) {
|
||||||
|
const { global } = props
|
||||||
|
fields = global.fields
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldSchemaToJSON(fields)
|
||||||
|
})
|
||||||
|
|
||||||
// The preview could either be an iframe embedded on the page
|
// The preview could either be an iframe embedded on the page
|
||||||
// Or it could be a separate popup window
|
// Or it could be a separate popup window
|
||||||
// We need to transmit data to both accordingly
|
// We need to transmit data to both accordingly
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fields && window && 'postMessage' in window) {
|
if (fields && window && 'postMessage' in window) {
|
||||||
const values = reduceFieldsToValues(fields)
|
const values = reduceFieldsToValues(fields, true)
|
||||||
const message = JSON.stringify({ data: values, type: 'livePreview' })
|
|
||||||
|
// TODO: only send `fieldSchemaToJSON` one time
|
||||||
|
const message = JSON.stringify({ data: values, fieldSchemaJSON, type: 'livePreview' })
|
||||||
|
|
||||||
// external window
|
// external window
|
||||||
if (isPopupOpen) {
|
if (isPopupOpen) {
|
||||||
@@ -66,6 +86,7 @@ const Preview: React.FC<
|
|||||||
popupHasLoaded,
|
popupHasLoaded,
|
||||||
iframeRef,
|
iframeRef,
|
||||||
setIframeHasLoaded,
|
setIframeHasLoaded,
|
||||||
|
fieldSchemaJSON,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!isPopupOpen) {
|
if (!isPopupOpen) {
|
||||||
|
|||||||
@@ -1,31 +1,45 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { mergeLivePreviewData } from './mergeData'
|
||||||
|
|
||||||
// To prevent the flicker of missing data on initial load,
|
// To prevent the flicker of missing data on initial load,
|
||||||
// you can pass in the initial page data from the server
|
// you can pass in the initial page data from the server
|
||||||
// To prevent the flicker of stale data while the post message is being sent,
|
// To prevent the flicker of stale data while the post message is being sent,
|
||||||
// you can conditionally render loading UI based on the `isLoading` state
|
// you can conditionally render loading UI based on the `isLoading` state
|
||||||
export const useLivePreview = (props: {
|
|
||||||
initialPage: any
|
export const useLivePreview = <T extends any>(props: {
|
||||||
|
depth?: number
|
||||||
|
initialPage: T
|
||||||
serverURL: string
|
serverURL: string
|
||||||
}): {
|
}): {
|
||||||
data: any
|
data: T
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
} => {
|
} => {
|
||||||
const { initialPage, serverURL } = props
|
const { depth = 0, initialPage, serverURL } = props
|
||||||
const [data, setData] = useState<any>(initialPage)
|
const [data, setData] = useState<T>(initialPage)
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||||
|
|
||||||
const handleMessage = useCallback(
|
const handleMessage = useCallback(
|
||||||
(event: MessageEvent) => {
|
async (event: MessageEvent) => {
|
||||||
if (event.origin === serverURL && event.data) {
|
if (event.origin === serverURL && event.data) {
|
||||||
const eventData = JSON.parse(event?.data)
|
const eventData = JSON.parse(event?.data)
|
||||||
|
|
||||||
if (eventData.type === 'livePreview') {
|
if (eventData.type === 'livePreview') {
|
||||||
setData(eventData?.data)
|
const mergedData = await mergeLivePreviewData<T>({
|
||||||
|
depth,
|
||||||
|
existingData: data,
|
||||||
|
fieldSchema: eventData.fieldSchemaJSON,
|
||||||
|
incomingData: eventData.data,
|
||||||
|
serverURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
setData(mergedData)
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[serverURL],
|
[serverURL, data, depth],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { traverseFields } from './traverseFields'
|
||||||
|
|
||||||
|
export type MergeLiveDataArgs<T> = {
|
||||||
|
apiRoute?: string
|
||||||
|
depth: number
|
||||||
|
existingData: T
|
||||||
|
fieldSchema: Record<string, unknown>[]
|
||||||
|
incomingData: T
|
||||||
|
serverURL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mergeLivePreviewData = async <T>({
|
||||||
|
apiRoute,
|
||||||
|
depth,
|
||||||
|
existingData,
|
||||||
|
fieldSchema,
|
||||||
|
incomingData,
|
||||||
|
serverURL,
|
||||||
|
}: MergeLiveDataArgs<T>): Promise<T> => {
|
||||||
|
const result = { ...existingData }
|
||||||
|
|
||||||
|
const populationPromises: Promise<void>[] = []
|
||||||
|
|
||||||
|
traverseFields({
|
||||||
|
apiRoute,
|
||||||
|
depth,
|
||||||
|
fieldSchema,
|
||||||
|
incomingData,
|
||||||
|
populationPromises,
|
||||||
|
result,
|
||||||
|
serverURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(populationPromises)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
type Args = {
|
||||||
|
accessor: number | string
|
||||||
|
apiRoute?: string
|
||||||
|
collection: string
|
||||||
|
depth: number
|
||||||
|
id: number | string
|
||||||
|
ref: Record<string, unknown>
|
||||||
|
serverURL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const promise = async ({
|
||||||
|
id,
|
||||||
|
accessor,
|
||||||
|
apiRoute,
|
||||||
|
collection,
|
||||||
|
depth,
|
||||||
|
ref,
|
||||||
|
serverURL,
|
||||||
|
}: Args): Promise<void> => {
|
||||||
|
// TODO: get dynamic `api` route from config
|
||||||
|
const res: any = await fetch(
|
||||||
|
`${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`,
|
||||||
|
).then((res) => res.json())
|
||||||
|
|
||||||
|
ref[accessor] = res
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { promise } from './promise'
|
||||||
|
|
||||||
|
type Args<T> = {
|
||||||
|
apiRoute?: string
|
||||||
|
depth: number
|
||||||
|
fieldSchema: Record<string, unknown>[]
|
||||||
|
incomingData: T
|
||||||
|
populationPromises: Promise<void>[]
|
||||||
|
result: T
|
||||||
|
serverURL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const traverseFields = <T>({
|
||||||
|
apiRoute,
|
||||||
|
depth,
|
||||||
|
fieldSchema,
|
||||||
|
incomingData,
|
||||||
|
populationPromises,
|
||||||
|
result,
|
||||||
|
serverURL,
|
||||||
|
}: Args<T>): void => {
|
||||||
|
fieldSchema.forEach((field) => {
|
||||||
|
if ('name' in field && typeof field.name === 'string') {
|
||||||
|
// TODO: type this
|
||||||
|
const fieldName = field.name
|
||||||
|
|
||||||
|
switch (field.type) {
|
||||||
|
case 'array':
|
||||||
|
if (Array.isArray(incomingData[fieldName])) {
|
||||||
|
result[fieldName] = incomingData[fieldName].map((row, i) => {
|
||||||
|
const hasExistingRow =
|
||||||
|
Array.isArray(result[fieldName]) &&
|
||||||
|
typeof result[fieldName][i] === 'object' &&
|
||||||
|
result[fieldName][i] !== null
|
||||||
|
|
||||||
|
const newRow = hasExistingRow ? { ...result[fieldName][i] } : {}
|
||||||
|
|
||||||
|
traverseFields({
|
||||||
|
apiRoute,
|
||||||
|
depth,
|
||||||
|
fieldSchema: field.fields as Record<string, unknown>[], // TODO: type this
|
||||||
|
incomingData: row,
|
||||||
|
populationPromises,
|
||||||
|
result: newRow,
|
||||||
|
serverURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
return newRow
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'blocks':
|
||||||
|
if (Array.isArray(incomingData[fieldName])) {
|
||||||
|
result[fieldName] = incomingData[fieldName].map((row, i) => {
|
||||||
|
const matchedBlock = field.blocks[row.blockType]
|
||||||
|
|
||||||
|
const hasExistingRow =
|
||||||
|
Array.isArray(result[fieldName]) &&
|
||||||
|
typeof result[fieldName][i] === 'object' &&
|
||||||
|
result[fieldName][i] !== null &&
|
||||||
|
result[fieldName][i].blockType === row.blockType
|
||||||
|
|
||||||
|
const newRow = hasExistingRow
|
||||||
|
? { ...result[fieldName][i] }
|
||||||
|
: {
|
||||||
|
blockType: matchedBlock.slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
traverseFields({
|
||||||
|
apiRoute,
|
||||||
|
depth,
|
||||||
|
fieldSchema: matchedBlock.fields as Record<string, unknown>[], // TODO: type this
|
||||||
|
incomingData: row,
|
||||||
|
populationPromises,
|
||||||
|
result: newRow,
|
||||||
|
serverURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
return newRow
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tab':
|
||||||
|
case 'group':
|
||||||
|
if (!result[fieldName]) {
|
||||||
|
result[fieldName] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverseFields({
|
||||||
|
apiRoute,
|
||||||
|
depth,
|
||||||
|
fieldSchema: field.fields as Record<string, unknown>[], // TODO: type this
|
||||||
|
incomingData: incomingData[fieldName] || {},
|
||||||
|
populationPromises,
|
||||||
|
result: result[fieldName],
|
||||||
|
serverURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'upload':
|
||||||
|
case 'relationship':
|
||||||
|
if (field.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||||
|
const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : []
|
||||||
|
result[fieldName] = Array.isArray(result[fieldName])
|
||||||
|
? [...result[fieldName]].slice(0, incomingData[fieldName].length)
|
||||||
|
: []
|
||||||
|
|
||||||
|
incomingData[fieldName].forEach((relation, i) => {
|
||||||
|
// Handle `hasMany` polymorphic
|
||||||
|
if (Array.isArray(field.relationTo)) {
|
||||||
|
const existingID = existingValue[i]?.value?.id
|
||||||
|
|
||||||
|
if (
|
||||||
|
existingID !== relation.value ||
|
||||||
|
existingValue[i]?.relationTo !== relation.relationTo
|
||||||
|
) {
|
||||||
|
result[fieldName][i] = {
|
||||||
|
relationTo: relation.relationTo,
|
||||||
|
}
|
||||||
|
|
||||||
|
populationPromises.push(
|
||||||
|
promise({
|
||||||
|
id: relation.value,
|
||||||
|
accessor: 'value',
|
||||||
|
apiRoute,
|
||||||
|
collection: relation.relationTo,
|
||||||
|
depth,
|
||||||
|
ref: result[fieldName][i],
|
||||||
|
serverURL,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle `hasMany` singular
|
||||||
|
const existingID = existingValue[i]?.id
|
||||||
|
|
||||||
|
if (existingID !== relation) {
|
||||||
|
populationPromises.push(
|
||||||
|
promise({
|
||||||
|
id: relation,
|
||||||
|
accessor: i,
|
||||||
|
apiRoute,
|
||||||
|
collection: String(field.relationTo),
|
||||||
|
depth,
|
||||||
|
ref: result[fieldName],
|
||||||
|
serverURL,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Handle `hasOne` polymorphic
|
||||||
|
if (Array.isArray(field.relationTo)) {
|
||||||
|
const hasNewValue =
|
||||||
|
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
|
||||||
|
const hasOldValue =
|
||||||
|
typeof result[fieldName] === 'object' && result[fieldName] !== null
|
||||||
|
|
||||||
|
const newValue = hasNewValue ? incomingData[fieldName].value : ''
|
||||||
|
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
|
||||||
|
|
||||||
|
const oldValue = hasOldValue ? result[fieldName].value : ''
|
||||||
|
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
|
||||||
|
|
||||||
|
if (newValue !== oldValue || newRelation !== oldRelation) {
|
||||||
|
if (newValue) {
|
||||||
|
if (!result[fieldName]) {
|
||||||
|
result[fieldName] = {
|
||||||
|
relationTo: newRelation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
populationPromises.push(
|
||||||
|
promise({
|
||||||
|
id: newValue,
|
||||||
|
accessor: 'value',
|
||||||
|
apiRoute,
|
||||||
|
collection: newRelation,
|
||||||
|
depth,
|
||||||
|
ref: result[fieldName],
|
||||||
|
serverURL,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[fieldName] = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const hasNewValue =
|
||||||
|
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
|
||||||
|
const hasOldValue =
|
||||||
|
typeof result[fieldName] === 'object' && result[fieldName] !== null
|
||||||
|
|
||||||
|
const newValue = hasNewValue ? incomingData[fieldName].value : ''
|
||||||
|
|
||||||
|
const oldValue = hasOldValue ? result[fieldName].value : ''
|
||||||
|
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
if (newValue) {
|
||||||
|
populationPromises.push(
|
||||||
|
promise({
|
||||||
|
id: newValue,
|
||||||
|
accessor: fieldName,
|
||||||
|
apiRoute,
|
||||||
|
collection: String(field.relationTo),
|
||||||
|
depth,
|
||||||
|
ref: result as Record<string, unknown>,
|
||||||
|
serverURL,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[fieldName] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result[fieldName] = incomingData[fieldName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -65,6 +65,59 @@ export default buildConfigWithDefaults({
|
|||||||
name: 'slug',
|
name: 'slug',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -78,12 +131,27 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const post1 = await payload.create({
|
||||||
|
collection: 'posts',
|
||||||
|
data: {
|
||||||
|
title: 'Post 1',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: slug,
|
collection: slug,
|
||||||
data: {
|
data: {
|
||||||
title: 'Hello, world!',
|
title: 'Hello, world!',
|
||||||
description: 'This is an example of live preview.',
|
description: 'This is an example of live preview.',
|
||||||
slug: 'home',
|
slug: 'home',
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
blockType: 'hero',
|
||||||
|
title: 'Hello, world!',
|
||||||
|
description: 'This is an example of live preview.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
featuredPosts: [post1.id],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
export type PageType = {
|
import { Page } from '@/payload-types'
|
||||||
title?: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PAYLOAD_SERVER_URL = 'http://localhost:3000'
|
export const PAYLOAD_SERVER_URL = 'http://localhost:3000'
|
||||||
|
|
||||||
export const getPage = async (slug: string): Promise<PageType> => {
|
export const getPage = async (slug: string): Promise<Page> => {
|
||||||
return await fetch(`http://localhost:3000/api/pages?where[slug][equals]=${slug}`, {
|
return await fetch(`http://localhost:3000/api/pages?where[slug][equals]=${slug}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import styles from './page.module.css'
|
import styles from './page.module.css'
|
||||||
import { PAYLOAD_SERVER_URL, PageType } from './api'
|
import { PAYLOAD_SERVER_URL } from './api'
|
||||||
// The `useLivePreview` hook is imported from the monorepo for development purposes only
|
// The `useLivePreview` hook is imported from the monorepo for development purposes only
|
||||||
// in your own app you would import this hook directly from the payload package itself
|
// in your own app you would import this hook directly from the payload package itself
|
||||||
// i.e. `import { useLivePreview } from 'payload'`
|
// i.e. `import { useLivePreview } from 'payload'`
|
||||||
import { useLivePreview } from '../../../../packages/payload/src/admin/components/views/LivePreview/useLivePreview'
|
import { useLivePreview } from '../../../../packages/payload/src/admin/components/views/LivePreview/useLivePreview'
|
||||||
|
import { Page as PageType } from '@/payload-types'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
initialPage: PageType
|
initialPage: PageType
|
||||||
@@ -14,15 +15,48 @@ export type Props = {
|
|||||||
|
|
||||||
export const Page: React.FC<Props> = (props) => {
|
export const Page: React.FC<Props> = (props) => {
|
||||||
const { initialPage } = props
|
const { initialPage } = props
|
||||||
const { data, isLoading } = useLivePreview({ initialPage, serverURL: PAYLOAD_SERVER_URL })
|
|
||||||
|
const { data, isLoading } = useLivePreview<PageType>({
|
||||||
|
initialPage,
|
||||||
|
serverURL: PAYLOAD_SERVER_URL,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
{isLoading && <Fragment>Loading...</Fragment>}
|
{isLoading && <Fragment>Loading...</Fragment>}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h1>{data?.title}</h1>
|
<h1>{data.title}</h1>
|
||||||
<p>{data?.description}</p>
|
<p>{data.description}</p>
|
||||||
|
{data.layout && (
|
||||||
|
<div>
|
||||||
|
<p>Blocks</p>
|
||||||
|
<div className={styles.blocks}>
|
||||||
|
{data.layout.map((block, index) => {
|
||||||
|
const { title, description } = block
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
|
<hr />
|
||||||
|
<br />
|
||||||
|
{data.featuredPosts && (
|
||||||
|
<div>
|
||||||
|
<p>Featured Posts</p>
|
||||||
|
<ul className={styles['featured-posts']}>
|
||||||
|
{data.featuredPosts.map((post, index) => (
|
||||||
|
<li key={index}>{typeof post === 'string' ? post : post.id}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -10,3 +10,9 @@
|
|||||||
.main > *:last-child {
|
.main > *:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.featured-posts {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
import { getPage } from './api'
|
import { getPage } from './api'
|
||||||
import { Page } from './page.client'
|
import { Page } from './page.client'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
|
import type { Metadata, ResolvingMetadata } from 'next'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: { id: string }
|
||||||
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(
|
||||||
|
{ params, searchParams }: Props,
|
||||||
|
parent: ResolvingMetadata,
|
||||||
|
): Promise<Metadata> {
|
||||||
|
const page = await getPage('home')
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: page.title,
|
||||||
|
description: page.description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const page = await getPage('home')
|
const page = await getPage('home')
|
||||||
|
|||||||
100
test/live-preview/next-app/payload-types.ts
Normal file
100
test/live-preview/next-app/payload-types.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
collections: {
|
||||||
|
users: User
|
||||||
|
pages: Page
|
||||||
|
posts: Post
|
||||||
|
'payload-preferences': PayloadPreference
|
||||||
|
'payload-migrations': PayloadMigration
|
||||||
|
}
|
||||||
|
globals: {}
|
||||||
|
}
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
email: string
|
||||||
|
resetPasswordToken?: string
|
||||||
|
resetPasswordExpiration?: string
|
||||||
|
salt?: string
|
||||||
|
hash?: string
|
||||||
|
loginAttempts?: number
|
||||||
|
lockUntil?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
export interface Page {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
slug: string
|
||||||
|
layout?: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
id?: string
|
||||||
|
blockName?: string
|
||||||
|
blockType: 'hero'
|
||||||
|
}[]
|
||||||
|
featuredPosts?: string[] | Post[]
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
export interface Post {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
export interface PayloadPreference {
|
||||||
|
id: string
|
||||||
|
user: {
|
||||||
|
relationTo: 'users'
|
||||||
|
value: string | User
|
||||||
|
}
|
||||||
|
key?: string
|
||||||
|
value?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
export interface PayloadMigration {
|
||||||
|
id: string
|
||||||
|
name?: string
|
||||||
|
batch?: number
|
||||||
|
schema?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'payload' {
|
||||||
|
export interface GeneratedTypes {
|
||||||
|
collections: {
|
||||||
|
users: User
|
||||||
|
pages: Page
|
||||||
|
posts: Post
|
||||||
|
'payload-preferences': PayloadPreference
|
||||||
|
'payload-migrations': PayloadMigration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export interface Config {
|
|||||||
collections: {
|
collections: {
|
||||||
users: User
|
users: User
|
||||||
pages: Page
|
pages: Page
|
||||||
|
posts: Post
|
||||||
'payload-preferences': PayloadPreference
|
'payload-preferences': PayloadPreference
|
||||||
'payload-migrations': PayloadMigration
|
'payload-migrations': PayloadMigration
|
||||||
}
|
}
|
||||||
@@ -33,6 +34,20 @@ export interface Page {
|
|||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
slug: string
|
slug: string
|
||||||
|
layout?: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
id?: string
|
||||||
|
blockName?: string
|
||||||
|
blockType: 'hero'
|
||||||
|
}[]
|
||||||
|
featuredPosts?: string[] | Post[]
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
export interface Post {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
@@ -77,6 +92,7 @@ declare module 'payload' {
|
|||||||
collections: {
|
collections: {
|
||||||
users: User
|
users: User
|
||||||
pages: Page
|
pages: Page
|
||||||
|
posts: Post
|
||||||
'payload-preferences': PayloadPreference
|
'payload-preferences': PayloadPreference
|
||||||
'payload-migrations': PayloadMigration
|
'payload-migrations': PayloadMigration
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user