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 { Field } from '../../../../../fields/config/types'
|
||||
import type { EditViewProps } from '../../types'
|
||||
import type { usePopupWindow } from '../usePopupWindow'
|
||||
|
||||
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
|
||||
import { useAllFormFields } from '../../../forms/Form/context'
|
||||
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
|
||||
import { LivePreviewProvider } from '../Context'
|
||||
@@ -33,13 +35,31 @@ const Preview: React.FC<
|
||||
|
||||
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
|
||||
// Or it could be a separate popup window
|
||||
// We need to transmit data to both accordingly
|
||||
useEffect(() => {
|
||||
if (fields && window && 'postMessage' in window) {
|
||||
const values = reduceFieldsToValues(fields)
|
||||
const message = JSON.stringify({ data: values, type: 'livePreview' })
|
||||
const values = reduceFieldsToValues(fields, true)
|
||||
|
||||
// TODO: only send `fieldSchemaToJSON` one time
|
||||
const message = JSON.stringify({ data: values, fieldSchemaJSON, type: 'livePreview' })
|
||||
|
||||
// external window
|
||||
if (isPopupOpen) {
|
||||
@@ -66,6 +86,7 @@ const Preview: React.FC<
|
||||
popupHasLoaded,
|
||||
iframeRef,
|
||||
setIframeHasLoaded,
|
||||
fieldSchemaJSON,
|
||||
])
|
||||
|
||||
if (!isPopupOpen) {
|
||||
|
||||
@@ -1,31 +1,45 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { mergeLivePreviewData } from './mergeData'
|
||||
|
||||
// To prevent the flicker of missing data on initial load,
|
||||
// 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,
|
||||
// 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
|
||||
}): {
|
||||
data: any
|
||||
data: T
|
||||
isLoading: boolean
|
||||
} => {
|
||||
const { initialPage, serverURL } = props
|
||||
const [data, setData] = useState<any>(initialPage)
|
||||
const { depth = 0, initialPage, serverURL } = props
|
||||
const [data, setData] = useState<T>(initialPage)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
async (event: MessageEvent) => {
|
||||
if (event.origin === serverURL && event.data) {
|
||||
const eventData = JSON.parse(event?.data)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
[serverURL],
|
||||
[serverURL, data, depth],
|
||||
)
|
||||
|
||||
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',
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -78,12 +131,27 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
})
|
||||
|
||||
const post1 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Post 1',
|
||||
},
|
||||
})
|
||||
|
||||
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],
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
export type PageType = {
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
import { Page } from '@/payload-types'
|
||||
|
||||
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}`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
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
|
||||
// in your own app you would import this hook directly from the payload package itself
|
||||
// i.e. `import { useLivePreview } from 'payload'`
|
||||
import { useLivePreview } from '../../../../packages/payload/src/admin/components/views/LivePreview/useLivePreview'
|
||||
import { Page as PageType } from '@/payload-types'
|
||||
|
||||
export type Props = {
|
||||
initialPage: PageType
|
||||
@@ -14,15 +15,48 @@ export type Props = {
|
||||
|
||||
export const Page: React.FC<Props> = (props) => {
|
||||
const { initialPage } = props
|
||||
const { data, isLoading } = useLivePreview({ initialPage, serverURL: PAYLOAD_SERVER_URL })
|
||||
|
||||
const { data, isLoading } = useLivePreview<PageType>({
|
||||
initialPage,
|
||||
serverURL: PAYLOAD_SERVER_URL,
|
||||
})
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
{isLoading && <Fragment>Loading...</Fragment>}
|
||||
{!isLoading && (
|
||||
<Fragment>
|
||||
<h1>{data?.title}</h1>
|
||||
<p>{data?.description}</p>
|
||||
<h1>{data.title}</h1>
|
||||
<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>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -10,3 +10,9 @@
|
||||
.main > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.featured-posts {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { getPage } from './api'
|
||||
import { Page } from './page.client'
|
||||
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() {
|
||||
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: {
|
||||
users: User
|
||||
pages: Page
|
||||
posts: Post
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
@@ -33,6 +34,20 @@ export interface Page {
|
||||
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
|
||||
}
|
||||
@@ -77,6 +92,7 @@ declare module 'payload' {
|
||||
collections: {
|
||||
users: User
|
||||
pages: Page
|
||||
posts: Post
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user