feat(live-preview): client-side live preview: simplify population, support hooks and lexical block population (#13619)
Alternative solution to https://github.com/payloadcms/payload/pull/11104. Big thanks to @andershermansen and @GermanJablo for kickstarting work on a solution and bringing this to our attention. This PR copies over the live-preview test suite example from his PR. Fixes https://github.com/payloadcms/payload/issues/5285, https://github.com/payloadcms/payload/issues/6071 and https://github.com/payloadcms/payload/issues/8277. Potentially fixes #11801 This PR completely gets rid of our client-side live preview field traversal + population and all logic related to it, and instead lets the findByID endpoint handle it. The data sent through the live preview message event is now passed to findByID via the newly added `data` attribute. The findByID endpoint will then use this data and run hooks on it (which run population), instead of fetching the data from the database. This new API basically behaves like a `/api/populate?data=` endpoint, with the benefit that it runs all the hooks. Another use-case for it will be rendering lexical data. Sometimes you may only have unpopulated data available. This functionality allows you to then populate the lexical portion of it on-the-fly, so that you can properly render it to JSX while displaying images. ## Benefits - a lot less code to maintain. No duplicative population logic - much faster - one single API request instead of one request per relationship to populate - all payload features are now correctly supported (population and hooks) - since hooks are now running for client-side live preview, this means the `lexicalHTML` field is now supported! This was a long-running issue - this fixes a lot of population inconsistencies that we previously did not know of. For example, it previously populated lexical and slate relationships even if the data was saved in an incorrect format ## [Method Override (POST)](https://payloadcms.com/docs/rest-api/overview#using-method-override-post) change The population request to the findByID endpoint is sent as a post request, so that we can pass through the `data` without having to squeeze it into the url params. To do that, it uses the `X-Payload-HTTP-Method-Override` header. Previously, this functionality still expected the data to be sent through as URL search params - just passed to the body instead of the URL. In this PR, I made it possible to pass it as JSON instead. This means: - the receiving endpoint will receive the data under `req.data` and is not able to read it from the search params - this means existing endpoints won't support this functionality unless they also attempt to read from req.data. - for the purpose of this PR, the findByID endpoint was modified to support this behavior. This functionality is documented as it can be useful for user-defined endpoints as well. Passing data as json has the following benefits: - it's more performant - no need to serialize and deserialize data to search params via `qs-esm`. This is especially important here, as we are passing large amounts of json data - the current implementation was serializing the data incorrectly, leading to incorrect data within nested lexical nodes **Note for people passing their own live preview `requestHandler`:** instead of sending a GET request to populate documents, you will now need to send a POST request to the findByID endpoint and pass additional headers. Additionally, you will need to send through the arguments as JSON instead of search params and include `data` as an argument. Here is the updated defaultRequestHandler for reference: ```ts const defaultRequestHandler: CollectionPopulationRequestHandler = ({ apiPath, data, endpoint, serverURL, }) => { const url = `${serverURL}${apiPath}/${endpoint}` return fetch(url, { body: JSON.stringify(data), credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-Payload-HTTP-Method-Override': 'GET', }, method: 'POST', }) } ``` --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211124793355068 - https://app.asana.com/0/0/1211124793355066
This commit is contained in:
@@ -773,3 +773,28 @@ const res = await fetch(`${api}/${collectionSlug}?depth=1&locale=en`, {
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Passing as JSON
|
||||
|
||||
When using `X-Payload-HTTP-Method-Override`, it expects the body to be a query string. If you want to pass JSON instead, you can set the `Content-Type` to `application/json` and include the JSON body in the request.
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
const res = await fetch(`${api}/${collectionSlug}/${id}`, {
|
||||
// Only the findByID endpoint supports HTTP method overrides with JSON data
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Payload-HTTP-Method-Override': 'GET',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
depth: 1,
|
||||
locale: 'en',
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
This can be more efficient for large JSON payloads, as you avoid converting data to and from query strings. However, only certain endpoints support this. Supported endpoints will read the parsed body under a `data` property, instead of reading from query parameters as with standard GET requests.
|
||||
|
||||
@@ -11,7 +11,7 @@ keywords: lexical, richtext, html
|
||||
There are two main approaches to convert your Lexical-based rich text to HTML:
|
||||
|
||||
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
|
||||
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API and may not work well with live preview.
|
||||
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API.
|
||||
|
||||
### On-demand
|
||||
|
||||
@@ -101,10 +101,7 @@ export const MyRSCComponent = async ({
|
||||
|
||||
### HTML field
|
||||
|
||||
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended for two reasons:
|
||||
|
||||
1. It creates a column with duplicate content in another format.
|
||||
2. In [client-side live preview](/docs/live-preview/client), it makes it not "live".
|
||||
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended, as it creates a column with duplicate content in another format.
|
||||
|
||||
Consider using the [on-demand HTML converter above](/docs/rich-text/converting-html#on-demand-recommended) or the [JSX converter](/docs/rich-text/converting-jsx) unless you have a good reason.
|
||||
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import type { FieldSchemaJSON } from 'payload'
|
||||
|
||||
import type { CollectionPopulationRequestHandler, LivePreviewMessageEvent } from './types.js'
|
||||
|
||||
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
|
||||
import { mergeData } from './mergeData.js'
|
||||
|
||||
const _payloadLivePreview: {
|
||||
fieldSchema: FieldSchemaJSON | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
previousData: any
|
||||
} = {
|
||||
/**
|
||||
* For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
|
||||
* We need to cache this value so that it can be used across subsequent messages
|
||||
* To do this, save `fieldSchemaJSON` when it arrives as a global variable
|
||||
* Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
|
||||
*/
|
||||
fieldSchema: undefined,
|
||||
/**
|
||||
* Each time the data is merged, cache the result as a `previousData` variable
|
||||
* This will ensure changes compound overtop of each other
|
||||
@@ -35,26 +25,13 @@ export const handleMessage = async <T extends Record<string, any>>(args: {
|
||||
const { apiRoute, depth, event, initialData, requestHandler, serverURL } = args
|
||||
|
||||
if (isLivePreviewEvent(event, serverURL)) {
|
||||
const { data, externallyUpdatedRelationship, fieldSchemaJSON, locale } = event.data
|
||||
|
||||
if (!_payloadLivePreview?.fieldSchema && fieldSchemaJSON) {
|
||||
_payloadLivePreview.fieldSchema = fieldSchemaJSON
|
||||
}
|
||||
|
||||
if (!_payloadLivePreview?.fieldSchema) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
|
||||
)
|
||||
|
||||
return initialData
|
||||
}
|
||||
const { collectionSlug, data, globalSlug, locale } = event.data
|
||||
|
||||
const mergedData = await mergeData<T>({
|
||||
apiRoute,
|
||||
collectionSlug,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: _payloadLivePreview.fieldSchema,
|
||||
globalSlug,
|
||||
incomingData: data,
|
||||
initialData: _payloadLivePreview?.previousData || initialData,
|
||||
locale,
|
||||
|
||||
@@ -4,6 +4,5 @@ export { isLivePreviewEvent } from './isLivePreviewEvent.js'
|
||||
export { mergeData } from './mergeData.js'
|
||||
export { ready } from './ready.js'
|
||||
export { subscribe } from './subscribe.js'
|
||||
export { traverseRichText } from './traverseRichText.js'
|
||||
export type { LivePreviewMessageEvent } from './types.js'
|
||||
export { unsubscribe } from './unsubscribe.js'
|
||||
|
||||
@@ -1,115 +1,60 @@
|
||||
import type { DocumentEvent, FieldSchemaJSON, PaginatedDocs } from 'payload'
|
||||
import type { CollectionPopulationRequestHandler } from './types.js'
|
||||
|
||||
import type { CollectionPopulationRequestHandler, PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
const defaultRequestHandler = ({
|
||||
const defaultRequestHandler: CollectionPopulationRequestHandler = ({
|
||||
apiPath,
|
||||
data,
|
||||
endpoint,
|
||||
serverURL,
|
||||
}: {
|
||||
apiPath: string
|
||||
endpoint: string
|
||||
serverURL: string
|
||||
}) => {
|
||||
const url = `${serverURL}${apiPath}/${endpoint}`
|
||||
|
||||
return fetch(url, {
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Payload-HTTP-Method-Override': 'GET',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// Relationships are only updated when their `id` or `relationTo` changes, by comparing the old and new values
|
||||
// This needs to also happen when locale changes, except this is not not part of the API response
|
||||
// Instead, we keep track of the old locale ourselves and trigger a re-population when it changes
|
||||
let prevLocale: string | undefined
|
||||
|
||||
export const mergeData = async <T extends Record<string, any>>(args: {
|
||||
apiRoute?: string
|
||||
/**
|
||||
* @deprecated Use `requestHandler` instead
|
||||
*/
|
||||
collectionPopulationRequestHandler?: CollectionPopulationRequestHandler
|
||||
collectionSlug?: string
|
||||
depth?: number
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchema: FieldSchemaJSON
|
||||
globalSlug?: string
|
||||
incomingData: Partial<T>
|
||||
initialData: T
|
||||
locale?: string
|
||||
requestHandler?: CollectionPopulationRequestHandler
|
||||
returnNumberOfRequests?: boolean
|
||||
serverURL: string
|
||||
}): Promise<
|
||||
{
|
||||
_numberOfRequests?: number
|
||||
} & T
|
||||
> => {
|
||||
}): Promise<T> => {
|
||||
const {
|
||||
apiRoute,
|
||||
collectionSlug,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema,
|
||||
globalSlug,
|
||||
incomingData,
|
||||
initialData,
|
||||
locale,
|
||||
returnNumberOfRequests,
|
||||
serverURL,
|
||||
} = args
|
||||
|
||||
const result = { ...initialData }
|
||||
const requestHandler = args.requestHandler || defaultRequestHandler
|
||||
|
||||
const populationsByCollection: PopulationsByCollection = {}
|
||||
const result = await requestHandler({
|
||||
apiPath: apiRoute || '/api',
|
||||
data: {
|
||||
data: incomingData,
|
||||
depth,
|
||||
locale,
|
||||
},
|
||||
endpoint: encodeURI(
|
||||
`${collectionSlug ?? globalSlug}${collectionSlug ? `/${initialData.id}` : ''}`,
|
||||
),
|
||||
serverURL,
|
||||
}).then((res) => res.json())
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema,
|
||||
incomingData,
|
||||
localeChanged: prevLocale !== locale,
|
||||
populationsByCollection,
|
||||
result,
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(populationsByCollection).map(async ([collection, populations]) => {
|
||||
let res: PaginatedDocs
|
||||
|
||||
const ids = new Set(populations.map(({ id }) => id))
|
||||
const requestHandler =
|
||||
args.collectionPopulationRequestHandler || args.requestHandler || defaultRequestHandler
|
||||
|
||||
try {
|
||||
res = await requestHandler({
|
||||
apiPath: apiRoute || '/api',
|
||||
endpoint: encodeURI(
|
||||
`${collection}?depth=${depth}&limit=${ids.size}&where[id][in]=${Array.from(ids).join(',')}${locale ? `&locale=${locale}` : ''}`,
|
||||
),
|
||||
serverURL,
|
||||
}).then((res) => res.json())
|
||||
|
||||
if (res?.docs?.length > 0) {
|
||||
res.docs.forEach((doc) => {
|
||||
populationsByCollection[collection]?.forEach((population) => {
|
||||
if (population.id === doc.id) {
|
||||
population.ref[population.accessor] = doc
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err) // eslint-disable-line no-console
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
prevLocale = locale
|
||||
|
||||
return {
|
||||
...result,
|
||||
...(returnNumberOfRequests
|
||||
? { _numberOfRequests: Object.keys(populationsByCollection).length }
|
||||
: {}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
|
||||
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseRichText } from './traverseRichText.js'
|
||||
|
||||
export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchema: FieldSchemaJSON
|
||||
incomingData: T
|
||||
localeChanged: boolean
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: Record<string, any>
|
||||
}): void => {
|
||||
const {
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchemas,
|
||||
incomingData,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
result,
|
||||
} = args
|
||||
|
||||
fieldSchemas.forEach((fieldSchema) => {
|
||||
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
|
||||
const fieldName = fieldSchema.name
|
||||
|
||||
switch (fieldSchema.type) {
|
||||
case 'array':
|
||||
if (
|
||||
!incomingData[fieldName] &&
|
||||
incomingData[fieldName] !== undefined &&
|
||||
result?.[fieldName] !== undefined
|
||||
) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
if (!result[fieldName][i]) {
|
||||
result[fieldName][i] = {}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields!,
|
||||
incomingData: incomingRow,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
result: result[fieldName][i],
|
||||
})
|
||||
|
||||
return result[fieldName][i]
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
|
||||
const incomingBlockJSON = fieldSchema.blocks?.[incomingBlock.blockType]
|
||||
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
if (
|
||||
!result[fieldName][i] ||
|
||||
result[fieldName][i].id !== incomingBlock.id ||
|
||||
result[fieldName][i].blockType !== incomingBlock.blockType
|
||||
) {
|
||||
result[fieldName][i] = {
|
||||
blockType: incomingBlock.blockType,
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: incomingBlockJSON!.fields!,
|
||||
incomingData: incomingBlock,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
result: result[fieldName][i],
|
||||
})
|
||||
|
||||
return result[fieldName][i]
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'group':
|
||||
// falls through
|
||||
case 'tabs':
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = {}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields!,
|
||||
incomingData: incomingData[fieldName] || {},
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
result: result[fieldName],
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'relationship':
|
||||
// falls through
|
||||
case 'upload':
|
||||
// Handle `hasMany` relationships
|
||||
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||
if (!result[fieldName] || !incomingData[fieldName].length) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
incomingData[fieldName].forEach((incomingRelation, i) => {
|
||||
// Handle `hasMany` polymorphic
|
||||
if (Array.isArray(fieldSchema.relationTo)) {
|
||||
// if the field doesn't exist on the result, create it
|
||||
// the value will be populated later
|
||||
if (!result[fieldName][i]) {
|
||||
result[fieldName][i] = {
|
||||
relationTo: incomingRelation.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
const oldID = result[fieldName][i]?.value?.id
|
||||
const oldRelation = result[fieldName][i]?.relationTo
|
||||
const newID = incomingRelation.value
|
||||
const newRelation = incomingRelation.relationTo
|
||||
|
||||
const hasChanged = newID !== oldID || newRelation !== oldRelation
|
||||
|
||||
const hasUpdated =
|
||||
newRelation === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
if (!populationsByCollection[newRelation]) {
|
||||
populationsByCollection[newRelation] = []
|
||||
}
|
||||
|
||||
populationsByCollection[newRelation].push({
|
||||
id: incomingRelation.value,
|
||||
accessor: 'value',
|
||||
ref: result[fieldName][i],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Handle `hasMany` monomorphic
|
||||
const hasChanged = incomingRelation !== result[fieldName][i]?.id
|
||||
|
||||
const hasUpdated =
|
||||
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
|
||||
incomingRelation === externallyUpdatedRelationship?.id
|
||||
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo!]) {
|
||||
populationsByCollection[fieldSchema.relationTo!] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo!]?.push({
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
ref: result[fieldName],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Handle `hasOne` polymorphic
|
||||
if (Array.isArray(fieldSchema.relationTo)) {
|
||||
// if the field doesn't exist on the result, create it
|
||||
// the value will be populated later
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = {
|
||||
relationTo: incomingData[fieldName]?.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
const hasNewValue =
|
||||
incomingData[fieldName] &&
|
||||
typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName] !== null
|
||||
|
||||
const hasOldValue =
|
||||
result[fieldName] &&
|
||||
typeof result[fieldName] === 'object' &&
|
||||
result[fieldName] !== null
|
||||
|
||||
const newID = hasNewValue
|
||||
? typeof incomingData[fieldName].value === 'object'
|
||||
? incomingData[fieldName].value.id
|
||||
: incomingData[fieldName].value
|
||||
: ''
|
||||
|
||||
const oldID = hasOldValue
|
||||
? typeof result[fieldName].value === 'object'
|
||||
? result[fieldName].value.id
|
||||
: result[fieldName].value
|
||||
: ''
|
||||
|
||||
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
|
||||
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
|
||||
|
||||
const hasChanged = newID !== oldID || newRelation !== oldRelation
|
||||
|
||||
const hasUpdated =
|
||||
newRelation === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
// if the new value/relation is different from the old value/relation
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
if (!populationsByCollection[newRelation]) {
|
||||
populationsByCollection[newRelation] = []
|
||||
}
|
||||
|
||||
populationsByCollection[newRelation].push({
|
||||
id: newID,
|
||||
accessor: 'value',
|
||||
ref: result[fieldName],
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle `hasOne` monomorphic
|
||||
const newID: number | string | undefined =
|
||||
(incomingData[fieldName] &&
|
||||
typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName].id) ||
|
||||
incomingData[fieldName]
|
||||
|
||||
const oldID: number | string | undefined =
|
||||
(result[fieldName] &&
|
||||
typeof result[fieldName] === 'object' &&
|
||||
result[fieldName].id) ||
|
||||
result[fieldName]
|
||||
|
||||
const hasChanged = newID !== oldID
|
||||
|
||||
const hasUpdated =
|
||||
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
// if the new value is different from the old value
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo!]) {
|
||||
populationsByCollection[fieldSchema.relationTo!] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo!]?.push({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
ref: result as Record<string, unknown>,
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
case 'richText':
|
||||
result[fieldName] = traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: incomingData[fieldName],
|
||||
populationsByCollection,
|
||||
result: result[fieldName],
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
result[fieldName] = incomingData[fieldName]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import type { DocumentEvent } from 'payload'
|
||||
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
export const traverseRichText = ({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData,
|
||||
populationsByCollection,
|
||||
result,
|
||||
}: {
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
incomingData: any
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: any
|
||||
}): any => {
|
||||
if (Array.isArray(incomingData)) {
|
||||
if (!result) {
|
||||
result = []
|
||||
}
|
||||
|
||||
result = incomingData.map((item, index) => {
|
||||
if (!result[index]) {
|
||||
result[index] = item
|
||||
}
|
||||
|
||||
return traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: item,
|
||||
populationsByCollection,
|
||||
result: result[index],
|
||||
})
|
||||
})
|
||||
} else if (incomingData && typeof incomingData === 'object') {
|
||||
if (!result) {
|
||||
result = {}
|
||||
}
|
||||
|
||||
// Remove keys from `result` that do not appear in `incomingData`
|
||||
// There's likely another way to do this,
|
||||
// But recursion and references make this very difficult
|
||||
Object.keys(result).forEach((key) => {
|
||||
if (!(key in incomingData)) {
|
||||
delete result[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Iterate over the keys of `incomingData` and populate `result`
|
||||
Object.keys(incomingData).forEach((key) => {
|
||||
if (!result[key]) {
|
||||
// Instantiate the key in `result` if it doesn't exist
|
||||
// Ensure its type matches the type of the `incomingData`
|
||||
// We don't have a schema to check against here
|
||||
result[key] =
|
||||
incomingData[key] && typeof incomingData[key] === 'object'
|
||||
? Array.isArray(incomingData[key])
|
||||
? []
|
||||
: {}
|
||||
: undefined
|
||||
}
|
||||
|
||||
const isRelationship = key === 'value' && 'relationTo' in incomingData
|
||||
|
||||
if (isRelationship) {
|
||||
// or if there are no keys besides id
|
||||
const needsPopulation =
|
||||
!result.value ||
|
||||
typeof result.value !== 'object' ||
|
||||
(typeof result.value === 'object' &&
|
||||
Object.keys(result.value).length === 1 &&
|
||||
'id' in result.value)
|
||||
|
||||
const hasChanged =
|
||||
result &&
|
||||
typeof result === 'object' &&
|
||||
result.value.id === externallyUpdatedRelationship?.id
|
||||
|
||||
if (needsPopulation || hasChanged) {
|
||||
if (!populationsByCollection[incomingData.relationTo]) {
|
||||
populationsByCollection[incomingData.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[incomingData.relationTo]?.push({
|
||||
id:
|
||||
incomingData[key] && typeof incomingData[key] === 'object'
|
||||
? incomingData[key].id
|
||||
: incomingData[key],
|
||||
accessor: 'value',
|
||||
ref: result,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
result[key] = traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: incomingData[key],
|
||||
populationsByCollection,
|
||||
result: result[key],
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
result = incomingData
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
|
||||
import type { DocumentEvent } from 'payload'
|
||||
|
||||
export type CollectionPopulationRequestHandler = ({
|
||||
apiPath,
|
||||
data,
|
||||
endpoint,
|
||||
serverURL,
|
||||
}: {
|
||||
apiPath: string
|
||||
data: Record<string, any>
|
||||
endpoint: string
|
||||
serverURL: string
|
||||
}) => Promise<Response>
|
||||
@@ -14,18 +16,11 @@ export type LivePreviewArgs = {}
|
||||
|
||||
export type LivePreview = void
|
||||
|
||||
export type PopulationsByCollection = {
|
||||
[slug: string]: Array<{
|
||||
accessor: number | string
|
||||
id: number | string
|
||||
ref: Record<string, unknown>
|
||||
}>
|
||||
}
|
||||
|
||||
export type LivePreviewMessageEvent<T> = MessageEvent<{
|
||||
collectionSlug?: string
|
||||
data: T
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchemaJSON: FieldSchemaJSON
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
type: 'payload-live-preview'
|
||||
}>
|
||||
|
||||
@@ -11,16 +11,21 @@ import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
|
||||
import { findByIDOperation } from '../operations/findByID.js'
|
||||
|
||||
export const findByIDHandler: PayloadHandler = async (req) => {
|
||||
const { searchParams } = req
|
||||
const { data, searchParams } = req
|
||||
const { id, collection } = getRequestCollectionWithID(req)
|
||||
const depth = searchParams.get('depth')
|
||||
const trash = searchParams.get('trash') === 'true'
|
||||
const depth = data ? data.depth : searchParams.get('depth')
|
||||
const trash = data ? data.trash : searchParams.get('trash') === 'true'
|
||||
|
||||
const result = await findByIDOperation({
|
||||
id,
|
||||
collection,
|
||||
data: data
|
||||
? data?.data
|
||||
: searchParams.get('data')
|
||||
? JSON.parse(searchParams.get('data') as string)
|
||||
: undefined,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft: searchParams.get('draft') === 'true',
|
||||
draft: data ? data.draft : searchParams.get('draft') === 'true',
|
||||
joins: sanitizeJoinParams(req.query.joins as JoinParams),
|
||||
populate: sanitizePopulateParam(req.query.populate),
|
||||
req,
|
||||
|
||||
@@ -32,6 +32,11 @@ import { buildAfterOperation } from './utils.js'
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
currentDepth?: number
|
||||
/**
|
||||
* You may pass the document data directly which will skip the `db.findOne` database query.
|
||||
* This is useful if you want to use this endpoint solely for running hooks and populating data.
|
||||
*/
|
||||
data?: Record<string, unknown>
|
||||
depth?: number
|
||||
disableErrors?: boolean
|
||||
draft?: boolean
|
||||
@@ -163,7 +168,8 @@ export const findByIDOperation = async <
|
||||
throw new NotFound(t)
|
||||
}
|
||||
|
||||
let result: DataFromCollectionSlug<TSlug> = (await req.payload.db.findOne(findOneArgs))!
|
||||
let result: DataFromCollectionSlug<TSlug> =
|
||||
(args.data as DataFromCollectionSlug<TSlug>) ?? (await req.payload.db.findOne(findOneArgs))!
|
||||
|
||||
if (!result) {
|
||||
if (!disableErrors) {
|
||||
|
||||
@@ -41,6 +41,11 @@ export type Options<
|
||||
* @internal
|
||||
*/
|
||||
currentDepth?: number
|
||||
/**
|
||||
* You may pass the document data directly which will skip the `db.findOne` database query.
|
||||
* This is useful if you want to use this endpoint solely for running hooks and populating data.
|
||||
*/
|
||||
data?: Record<string, unknown>
|
||||
/**
|
||||
* [Control auto-population](https://payloadcms.com/docs/queries/depth) of nested relationship and upload fields.
|
||||
*/
|
||||
@@ -126,6 +131,7 @@ export async function findByIDLocal<
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
currentDepth,
|
||||
data,
|
||||
depth,
|
||||
disableErrors = false,
|
||||
draft = false,
|
||||
@@ -150,6 +156,7 @@ export async function findByIDLocal<
|
||||
id,
|
||||
collection,
|
||||
currentDepth,
|
||||
data,
|
||||
depth,
|
||||
disableErrors,
|
||||
draft,
|
||||
|
||||
@@ -75,8 +75,6 @@ export {
|
||||
|
||||
export { extractID } from '../utilities/extractID.js'
|
||||
|
||||
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
|
||||
|
||||
export { flattenAllFields } from '../utilities/flattenAllFields.js'
|
||||
export { flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
|
||||
export { formatAdminURL } from '../utilities/formatAdminURL.js'
|
||||
|
||||
@@ -1671,7 +1671,6 @@ export {
|
||||
type CustomVersionParser,
|
||||
} from './utilities/dependencies/dependencyChecker.js'
|
||||
export { getDependencies } from './utilities/dependencies/getDependencies.js'
|
||||
export type { FieldSchemaJSON } from './utilities/fieldSchemaToJSON.js'
|
||||
export {
|
||||
findUp,
|
||||
findUpSync,
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import type { ClientConfig } from '../config/client.js'
|
||||
import type { ClientField } from '../fields/config/client.js'
|
||||
import type { Field, FieldTypes } from '../fields/config/types.js'
|
||||
|
||||
import { fieldAffectsData } from '../fields/config/types.js'
|
||||
|
||||
export type FieldSchemaJSON = {
|
||||
blocks?: FieldSchemaJSON // TODO: conditionally add based on `type`
|
||||
fields?: FieldSchemaJSON // TODO: conditionally add based on `type`
|
||||
hasMany?: boolean // TODO: conditionally add based on `type`
|
||||
name: string
|
||||
relationTo?: string // TODO: conditionally add based on `type`
|
||||
slug?: string // TODO: conditionally add based on `type`
|
||||
type: FieldTypes
|
||||
}[]
|
||||
|
||||
export const fieldSchemaToJSON = (
|
||||
fields: (ClientField | Field)[],
|
||||
config: ClientConfig,
|
||||
): FieldSchemaJSON => {
|
||||
return fields.reduce((acc, field) => {
|
||||
let result = acc
|
||||
|
||||
switch (field.type) {
|
||||
case 'array':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON(
|
||||
[
|
||||
...field.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
config,
|
||||
),
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
blocks: (field.blockReferences ?? field.blocks).reduce((acc, _block) => {
|
||||
const block = typeof _block === 'string' ? config.blocksMap[_block]! : _block
|
||||
;(acc as any)[block.slug] = {
|
||||
fields: fieldSchemaToJSON(
|
||||
[
|
||||
...block.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
config,
|
||||
),
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {} as FieldSchemaJSON),
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'collapsible': // eslint-disable no-fallthrough
|
||||
case 'row':
|
||||
result = result.concat(fieldSchemaToJSON(field.fields, config))
|
||||
break
|
||||
|
||||
case 'group':
|
||||
if (fieldAffectsData(field)) {
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON(field.fields, config),
|
||||
})
|
||||
} else {
|
||||
result = result.concat(fieldSchemaToJSON(field.fields, config))
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'relationship': // eslint-disable no-fallthrough
|
||||
case 'upload':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
hasMany: 'hasMany' in field ? Boolean(field.hasMany) : false, // TODO: type this
|
||||
relationTo: field.relationTo as string,
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'tabs': {
|
||||
let tabFields: FieldSchemaJSON = []
|
||||
|
||||
field.tabs.forEach((tab) => {
|
||||
if ('name' in tab) {
|
||||
tabFields.push({
|
||||
name: tab.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON(tab.fields, config),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tabFields = tabFields.concat(fieldSchemaToJSON(tab.fields, config))
|
||||
})
|
||||
|
||||
result = result.concat(tabFields)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
if ('name' in field) {
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, [] as FieldSchemaJSON)
|
||||
}
|
||||
@@ -84,21 +84,44 @@ export const handleEndpoints = async ({
|
||||
(request.headers.get('X-Payload-HTTP-Method-Override') === 'GET' ||
|
||||
request.headers.get('X-HTTP-Method-Override') === 'GET')
|
||||
) {
|
||||
const search = await request.text()
|
||||
let url = request.url
|
||||
let data: any = undefined
|
||||
if (request.headers.get('Content-Type') === 'application/x-www-form-urlencoded') {
|
||||
const search = await request.text()
|
||||
url = `${request.url}?${search}`
|
||||
} else if (request.headers.get('Content-Type') === 'application/json') {
|
||||
// May not be supported by every endpoint
|
||||
data = await request.json()
|
||||
|
||||
// locale and fallbackLocale is read by createPayloadRequest to populate req.locale and req.fallbackLocale
|
||||
// => add to searchParams
|
||||
if (data?.locale) {
|
||||
url += `?locale=${data.locale}`
|
||||
}
|
||||
if (data?.fallbackLocale) {
|
||||
url += `&fallbackLocale=${data.depth}`
|
||||
}
|
||||
}
|
||||
|
||||
const req = new Request(url, {
|
||||
// @ts-expect-error // TODO: check if this is required
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
headers: request.headers,
|
||||
method: 'GET',
|
||||
signal: request.signal,
|
||||
})
|
||||
|
||||
if (data) {
|
||||
// @ts-expect-error attach data to request - less overhead than using urlencoded
|
||||
req.data = data
|
||||
}
|
||||
|
||||
const url = `${request.url}?${new URLSearchParams(search).toString()}`
|
||||
const response = await handleEndpoints({
|
||||
basePath,
|
||||
config: incomingConfig,
|
||||
path,
|
||||
request: new Request(url, {
|
||||
// @ts-expect-error // TODO: check if this is required
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
headers: request.headers,
|
||||
method: 'GET',
|
||||
signal: request.signal,
|
||||
}),
|
||||
request: req,
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
@@ -7,13 +7,14 @@ import React, { useEffect } from 'react'
|
||||
|
||||
import { useAllFormFields } from '../../../forms/Form/context.js'
|
||||
import { useDocumentEvents } from '../../../providers/DocumentEvents/index.js'
|
||||
import { useDocumentInfo } from '../../../providers/DocumentInfo/index.js'
|
||||
import { useLivePreviewContext } from '../../../providers/LivePreview/context.js'
|
||||
import { useLocale } from '../../../providers/Locale/index.js'
|
||||
import { ShimmerEffect } from '../../ShimmerEffect/index.js'
|
||||
import { DeviceContainer } from '../Device/index.js'
|
||||
import './index.scss'
|
||||
import { IFrame } from '../IFrame/index.js'
|
||||
import { LivePreviewToolbar } from '../Toolbar/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'live-preview-window'
|
||||
|
||||
@@ -21,7 +22,6 @@ export const LivePreviewWindow: React.FC<EditViewProps> = (props) => {
|
||||
const {
|
||||
appIsReady,
|
||||
breakpoint,
|
||||
fieldSchemaJSON,
|
||||
iframeHasLoaded,
|
||||
iframeRef,
|
||||
isLivePreviewing,
|
||||
@@ -35,10 +35,8 @@ export const LivePreviewWindow: React.FC<EditViewProps> = (props) => {
|
||||
|
||||
const { mostRecentUpdate } = useDocumentEvents()
|
||||
|
||||
const prevWindowType =
|
||||
React.useRef<ReturnType<typeof useLivePreviewContext>['previewWindowType']>(undefined)
|
||||
|
||||
const [formState] = useAllFormFields()
|
||||
const { id, collectionSlug, globalSlug } = useDocumentInfo()
|
||||
|
||||
// For client-side apps, send data through `window.postMessage`
|
||||
// The preview could either be an iframe embedded on the page
|
||||
@@ -53,20 +51,16 @@ export const LivePreviewWindow: React.FC<EditViewProps> = (props) => {
|
||||
if (formState && window && 'postMessage' in window && appIsReady) {
|
||||
const values = reduceFieldsToValues(formState, true)
|
||||
|
||||
// To reduce on large `postMessage` payloads, only send `fieldSchemaToJSON` one time
|
||||
// To do this, the underlying JS function maintains a cache of this value
|
||||
// So we need to send it through each time the window type changes
|
||||
// But only once per window type change, not on every render, because this is a potentially large obj
|
||||
const shouldSendSchema =
|
||||
!prevWindowType.current || prevWindowType.current !== previewWindowType
|
||||
|
||||
prevWindowType.current = previewWindowType
|
||||
if (!values.id) {
|
||||
values.id = id
|
||||
}
|
||||
|
||||
const message = {
|
||||
type: 'payload-live-preview',
|
||||
collectionSlug,
|
||||
data: values,
|
||||
externallyUpdatedRelationship: mostRecentUpdate,
|
||||
fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
|
||||
globalSlug,
|
||||
locale: locale.code,
|
||||
}
|
||||
|
||||
@@ -83,13 +77,15 @@ export const LivePreviewWindow: React.FC<EditViewProps> = (props) => {
|
||||
}, [
|
||||
formState,
|
||||
url,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
iframeHasLoaded,
|
||||
id,
|
||||
previewWindowType,
|
||||
popupRef,
|
||||
appIsReady,
|
||||
iframeRef,
|
||||
setIframeHasLoaded,
|
||||
fieldSchemaJSON,
|
||||
mostRecentUpdate,
|
||||
locale,
|
||||
isLivePreviewing,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { LivePreviewConfig } from 'payload'
|
||||
import type { fieldSchemaToJSON } from 'payload/shared'
|
||||
import type { Dispatch } from 'react'
|
||||
import type React from 'react'
|
||||
|
||||
@@ -13,7 +12,6 @@ export interface LivePreviewContextType {
|
||||
appIsReady: boolean
|
||||
breakpoint: LivePreviewConfig['breakpoints'][number]['name']
|
||||
breakpoints: LivePreviewConfig['breakpoints']
|
||||
fieldSchemaJSON?: ReturnType<typeof fieldSchemaToJSON>
|
||||
iframeHasLoaded: boolean
|
||||
iframeRef: React.RefObject<HTMLIFrameElement | null>
|
||||
isLivePreviewEnabled: boolean
|
||||
@@ -54,7 +52,6 @@ export const LivePreviewContext = createContext<LivePreviewContextType>({
|
||||
appIsReady: false,
|
||||
breakpoint: undefined,
|
||||
breakpoints: undefined,
|
||||
fieldSchemaJSON: undefined,
|
||||
iframeHasLoaded: false,
|
||||
iframeRef: undefined,
|
||||
isLivePreviewEnabled: undefined,
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
import type { CollectionPreferences, LivePreviewConfig } from 'payload'
|
||||
|
||||
import { DndContext } from '@dnd-kit/core'
|
||||
import { fieldSchemaToJSON } from 'payload/shared'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { usePopupWindow } from '../../hooks/usePopupWindow.js'
|
||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||
import { usePreferences } from '../../providers/Preferences/index.js'
|
||||
import { useConfig } from '../Config/index.js'
|
||||
import { customCollisionDetection } from './collisionDetection.js'
|
||||
import { LivePreviewContext } from './context.js'
|
||||
import { sizeReducer } from './sizeReducer.js'
|
||||
@@ -90,8 +88,6 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
|
||||
|
||||
const [iframeHasLoaded, setIframeHasLoaded] = useState(false)
|
||||
|
||||
const { config, getEntityConfig } = useConfig()
|
||||
|
||||
const [zoom, setZoom] = useState(1)
|
||||
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
@@ -103,13 +99,9 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
|
||||
width: 0,
|
||||
})
|
||||
|
||||
const entityConfig = getEntityConfig({ collectionSlug, globalSlug })
|
||||
|
||||
const [breakpoint, setBreakpoint] =
|
||||
React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive')
|
||||
|
||||
const [fieldSchemaJSON] = useState(() => fieldSchemaToJSON(entityConfig?.fields || [], config))
|
||||
|
||||
// The toolbar needs to freely drag and drop around the page
|
||||
const handleDragEnd = (ev) => {
|
||||
// only update position if the toolbar is completely within the preview area
|
||||
@@ -232,7 +224,6 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
|
||||
appIsReady,
|
||||
breakpoint,
|
||||
breakpoints,
|
||||
fieldSchemaJSON,
|
||||
iframeHasLoaded,
|
||||
iframeRef,
|
||||
isLivePreviewEnabled,
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -44,7 +44,7 @@ importers:
|
||||
version: 1.54.1
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.4.4(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.4.4(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
|
||||
'@sentry/node':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1
|
||||
@@ -1143,7 +1143,7 @@ importers:
|
||||
dependencies:
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.4.4(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.4.4(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
|
||||
'@sentry/types':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1
|
||||
@@ -2045,7 +2045,7 @@ importers:
|
||||
version: link:../packages/ui
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.4.4(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.4.4(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
|
||||
'@sentry/react':
|
||||
specifier: ^7.77.0
|
||||
version: 7.119.2(react@19.1.0)
|
||||
@@ -16606,7 +16606,7 @@ snapshots:
|
||||
'@sentry/utils': 7.119.2
|
||||
localforage: 1.10.0
|
||||
|
||||
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.4.4(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))':
|
||||
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.4.4(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0)
|
||||
|
||||
@@ -36,7 +36,9 @@ export async function generateStaticParams() {
|
||||
|
||||
try {
|
||||
const pages = await getDocs<Page>('pages')
|
||||
return pages?.map(({ slug }) => slug)
|
||||
return pages?.map((page) => {
|
||||
return { slug: page.slug }
|
||||
})
|
||||
} catch (_err) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ export async function generateStaticParams() {
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'false'
|
||||
try {
|
||||
const ssrPosts = await getDocs<Post>(postsSlug)
|
||||
return ssrPosts?.map(({ slug }) => slug)
|
||||
return ssrPosts?.map((page) => {
|
||||
return { slug: page.slug }
|
||||
})
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ export async function generateStaticParams() {
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'false'
|
||||
try {
|
||||
const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug)
|
||||
return ssrPages?.map(({ slug }) => slug)
|
||||
return ssrPages?.map((page) => {
|
||||
return { slug: page.slug }
|
||||
})
|
||||
} catch (_err) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ export async function generateStaticParams() {
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'false'
|
||||
try {
|
||||
const ssrPages = await getDocs<Page>(ssrPagesSlug)
|
||||
return ssrPages?.map(({ slug }) => slug)
|
||||
return ssrPages?.map((page) => {
|
||||
return { slug: page.slug }
|
||||
})
|
||||
} catch (_err) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const RelationshipsBlock: React.FC<RelationshipsBlockProps> = (props) =>
|
||||
<b>Rich Text — Lexical:</b>
|
||||
</p>
|
||||
{data?.richTextLexical && (
|
||||
<RichText content={data.richTextLexical} renderUploadFilenameOnly serializer="lexical" />
|
||||
<RichText content={data.richTextLexical} renderUploadFilenameOnly />
|
||||
)}
|
||||
<p>
|
||||
<b>Upload:</b>
|
||||
|
||||
@@ -52,6 +52,8 @@ export const Image: React.FC<MediaProps> = (props) => {
|
||||
src = `${PAYLOAD_SERVER_URL}/api/media/file/${filename}`
|
||||
}
|
||||
|
||||
if (!src) return null
|
||||
|
||||
// NOTE: this is used by the browser to determine which image to download at different screen sizes
|
||||
const sizes = Object.entries(breakpoints)
|
||||
.map(([, value]) => `(max-width: ${value}px) ${value}px`)
|
||||
|
||||
@@ -8,12 +8,12 @@ const RichText: React.FC<{
|
||||
className?: string
|
||||
content: any
|
||||
renderUploadFilenameOnly?: boolean
|
||||
serializer?: 'lexical' | 'slate'
|
||||
}> = ({ className, content, renderUploadFilenameOnly, serializer = 'slate' }) => {
|
||||
}> = ({ className, content, renderUploadFilenameOnly }) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
const serializer = Array.isArray(content) ? 'slate' : 'lexical'
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serializer === 'slate'
|
||||
@@ -22,5 +22,4 @@ const RichText: React.FC<{
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { CMSLink } from '../Link/index.js'
|
||||
import { Media } from '../Media/index.js'
|
||||
|
||||
import { MediaBlock } from '../../_blocks/MediaBlock/index.js'
|
||||
const serializer = (
|
||||
content?: SerializedEditorState['root']['children'],
|
||||
content?: DefaultTypedEditorState['root']['children'],
|
||||
renderUploadFilenameOnly?: boolean,
|
||||
): React.ReactNode | React.ReactNode[] =>
|
||||
content?.map((node, i) => {
|
||||
@@ -79,11 +79,17 @@ const serializer = (
|
||||
}
|
||||
|
||||
return <Media key={i} resource={node?.value} />
|
||||
|
||||
case 'block':
|
||||
switch (node.fields.blockType) {
|
||||
case 'mediaBlock':
|
||||
return <MediaBlock key={i} {...node.fields} />
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const serializeLexical = (
|
||||
content?: SerializedEditorState,
|
||||
content?: DefaultTypedEditorState,
|
||||
renderUploadFilenameOnly?: boolean,
|
||||
): React.ReactNode | React.ReactNode[] => {
|
||||
return serializer(content?.root?.children, renderUploadFilenameOnly)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { BlocksFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
|
||||
import { Archive } from '../blocks/ArchiveBlock/index.js'
|
||||
@@ -85,7 +85,12 @@ export const Pages: CollectionConfig = {
|
||||
label: 'Rich Text — Lexical',
|
||||
type: 'richText',
|
||||
name: 'richTextLexical',
|
||||
editor: lexicalEditor({}),
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({ blocks: ['mediaBlock'] }),
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'relationshipAsUpload',
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { MediaBlock } from './blocks/MediaBlock/index.js'
|
||||
import { Categories } from './collections/Categories.js'
|
||||
import { CollectionLevelConfig } from './collections/CollectionLevelConfig.js'
|
||||
import { Media } from './collections/Media.js'
|
||||
@@ -61,4 +62,5 @@ export default buildConfigWithDefaults({
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
blocks: [MediaBlock],
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,7 +65,9 @@ export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
blocks: {
|
||||
mediaBlock: MediaBlock;
|
||||
};
|
||||
collections: {
|
||||
users: User;
|
||||
pages: Page;
|
||||
@@ -96,7 +98,7 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {
|
||||
header: Header;
|
||||
@@ -133,12 +135,43 @@ export interface UserAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "mediaBlock".
|
||||
*/
|
||||
export interface MediaBlock {
|
||||
invertBackground?: boolean | null;
|
||||
position?: ('default' | 'fullscreen') | null;
|
||||
media: number | Media;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'mediaBlock';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: number;
|
||||
alt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -162,9 +195,9 @@ export interface User {
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
tenant?: (number | null) | Tenant;
|
||||
title: string;
|
||||
hero: {
|
||||
type: 'none' | 'highImpact' | 'lowImpact';
|
||||
@@ -173,7 +206,7 @@ export interface Page {
|
||||
[k: string]: unknown;
|
||||
}[]
|
||||
| null;
|
||||
media?: (string | null) | Media;
|
||||
media?: (number | null) | Media;
|
||||
};
|
||||
layout?:
|
||||
| (
|
||||
@@ -192,11 +225,11 @@ export interface Page {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -229,11 +262,11 @@ export interface Page {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -252,7 +285,7 @@ export interface Page {
|
||||
| {
|
||||
invertBackground?: boolean | null;
|
||||
position?: ('default' | 'fullscreen') | null;
|
||||
media: string | Media;
|
||||
media: number | Media;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'mediaBlock';
|
||||
@@ -265,12 +298,12 @@ export interface Page {
|
||||
| null;
|
||||
populateBy?: ('collection' | 'selection') | null;
|
||||
relationTo?: 'posts' | null;
|
||||
categories?: (string | Category)[] | null;
|
||||
categories?: (number | Category)[] | null;
|
||||
limit?: number | null;
|
||||
selectedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
@@ -279,7 +312,7 @@ export interface Page {
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
@@ -293,7 +326,7 @@ export interface Page {
|
||||
)[]
|
||||
| null;
|
||||
localizedTitle?: string | null;
|
||||
relationToLocalized?: (string | null) | Post;
|
||||
relationToLocalized?: (number | null) | Post;
|
||||
richTextSlate?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
@@ -314,22 +347,22 @@ export interface Page {
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
relationshipAsUpload?: (string | null) | Media;
|
||||
relationshipMonoHasOne?: (string | null) | Post;
|
||||
relationshipMonoHasMany?: (string | Post)[] | null;
|
||||
relationshipAsUpload?: (number | null) | Media;
|
||||
relationshipMonoHasOne?: (number | null) | Post;
|
||||
relationshipMonoHasMany?: (number | Post)[] | null;
|
||||
relationshipPolyHasOne?: {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null;
|
||||
relationshipPolyHasMany?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
arrayOfRelationships?:
|
||||
| {
|
||||
uploadInArray?: (string | null) | Media;
|
||||
uploadInArray?: (number | null) | Media;
|
||||
richTextInArray?: {
|
||||
root: {
|
||||
type: string;
|
||||
@@ -345,28 +378,28 @@ export interface Page {
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
relationshipInArrayMonoHasOne?: (string | null) | Post;
|
||||
relationshipInArrayMonoHasMany?: (string | Post)[] | null;
|
||||
relationshipInArrayMonoHasOne?: (number | null) | Post;
|
||||
relationshipInArrayMonoHasMany?: (number | Post)[] | null;
|
||||
relationshipInArrayPolyHasOne?: {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null;
|
||||
relationshipInArrayPolyHasMany?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
tab?: {
|
||||
relationshipInTab?: (string | null) | Post;
|
||||
relationshipInTab?: (number | null) | Post;
|
||||
};
|
||||
meta?: {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
image?: (string | null) | Media;
|
||||
image?: (number | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -376,39 +409,20 @@ export interface Page {
|
||||
* via the `definition` "tenants".
|
||||
*/
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
id: number;
|
||||
title: string;
|
||||
clientURL: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
alt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
tenant?: (number | null) | Tenant;
|
||||
title: string;
|
||||
hero: {
|
||||
type: 'none' | 'highImpact' | 'lowImpact';
|
||||
@@ -417,7 +431,7 @@ export interface Post {
|
||||
[k: string]: unknown;
|
||||
}[]
|
||||
| null;
|
||||
media?: (string | null) | Media;
|
||||
media?: (number | null) | Media;
|
||||
};
|
||||
layout?:
|
||||
| (
|
||||
@@ -436,11 +450,11 @@ export interface Post {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -473,11 +487,11 @@ export interface Post {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -496,7 +510,7 @@ export interface Post {
|
||||
| {
|
||||
invertBackground?: boolean | null;
|
||||
position?: ('default' | 'fullscreen') | null;
|
||||
media: string | Media;
|
||||
media: number | Media;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'mediaBlock';
|
||||
@@ -509,12 +523,12 @@ export interface Post {
|
||||
| null;
|
||||
populateBy?: ('collection' | 'selection') | null;
|
||||
relationTo?: 'posts' | null;
|
||||
categories?: (string | Category)[] | null;
|
||||
categories?: (number | Category)[] | null;
|
||||
limit?: number | null;
|
||||
selectedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
@@ -523,7 +537,7 @@ export interface Post {
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
@@ -536,12 +550,12 @@ export interface Post {
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
relatedPosts?: (string | Post)[] | null;
|
||||
relatedPosts?: (number | Post)[] | null;
|
||||
localizedTitle?: string | null;
|
||||
meta?: {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
image?: (string | null) | Media;
|
||||
image?: (number | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -552,7 +566,7 @@ export interface Post {
|
||||
* via the `definition` "categories".
|
||||
*/
|
||||
export interface Category {
|
||||
id: string;
|
||||
id: number;
|
||||
title?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -562,9 +576,9 @@ export interface Category {
|
||||
* via the `definition` "ssr".
|
||||
*/
|
||||
export interface Ssr {
|
||||
id: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
tenant?: (number | null) | Tenant;
|
||||
title: string;
|
||||
hero: {
|
||||
type: 'none' | 'highImpact' | 'lowImpact';
|
||||
@@ -573,7 +587,7 @@ export interface Ssr {
|
||||
[k: string]: unknown;
|
||||
}[]
|
||||
| null;
|
||||
media?: (string | null) | Media;
|
||||
media?: (number | null) | Media;
|
||||
};
|
||||
layout?:
|
||||
| (
|
||||
@@ -592,11 +606,11 @@ export interface Ssr {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -629,11 +643,11 @@ export interface Ssr {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -652,7 +666,7 @@ export interface Ssr {
|
||||
| {
|
||||
invertBackground?: boolean | null;
|
||||
position?: ('default' | 'fullscreen') | null;
|
||||
media: string | Media;
|
||||
media: number | Media;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'mediaBlock';
|
||||
@@ -665,12 +679,12 @@ export interface Ssr {
|
||||
| null;
|
||||
populateBy?: ('collection' | 'selection') | null;
|
||||
relationTo?: 'posts' | null;
|
||||
categories?: (string | Category)[] | null;
|
||||
categories?: (number | Category)[] | null;
|
||||
limit?: number | null;
|
||||
selectedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
@@ -679,7 +693,7 @@ export interface Ssr {
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
@@ -695,7 +709,7 @@ export interface Ssr {
|
||||
meta?: {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
image?: (string | null) | Media;
|
||||
image?: (number | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -705,9 +719,9 @@ export interface Ssr {
|
||||
* via the `definition` "ssr-autosave".
|
||||
*/
|
||||
export interface SsrAutosave {
|
||||
id: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
tenant?: (number | null) | Tenant;
|
||||
title: string;
|
||||
hero: {
|
||||
type: 'none' | 'highImpact' | 'lowImpact';
|
||||
@@ -716,7 +730,7 @@ export interface SsrAutosave {
|
||||
[k: string]: unknown;
|
||||
}[]
|
||||
| null;
|
||||
media?: (string | null) | Media;
|
||||
media?: (number | null) | Media;
|
||||
};
|
||||
layout?:
|
||||
| (
|
||||
@@ -735,11 +749,11 @@ export interface SsrAutosave {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -772,11 +786,11 @@ export interface SsrAutosave {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -795,7 +809,7 @@ export interface SsrAutosave {
|
||||
| {
|
||||
invertBackground?: boolean | null;
|
||||
position?: ('default' | 'fullscreen') | null;
|
||||
media: string | Media;
|
||||
media: number | Media;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'mediaBlock';
|
||||
@@ -808,12 +822,12 @@ export interface SsrAutosave {
|
||||
| null;
|
||||
populateBy?: ('collection' | 'selection') | null;
|
||||
relationTo?: 'posts' | null;
|
||||
categories?: (string | Category)[] | null;
|
||||
categories?: (number | Category)[] | null;
|
||||
limit?: number | null;
|
||||
selectedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
@@ -822,7 +836,7 @@ export interface SsrAutosave {
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
@@ -838,7 +852,7 @@ export interface SsrAutosave {
|
||||
meta?: {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
image?: (string | null) | Media;
|
||||
image?: (number | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -851,7 +865,7 @@ export interface SsrAutosave {
|
||||
* via the `definition` "collection-level-config".
|
||||
*/
|
||||
export interface CollectionLevelConfig {
|
||||
id: string;
|
||||
id: number;
|
||||
title?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -861,48 +875,48 @@ export interface CollectionLevelConfig {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'ssr';
|
||||
value: string | Ssr;
|
||||
value: number | Ssr;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'ssr-autosave';
|
||||
value: string | SsrAutosave;
|
||||
value: number | SsrAutosave;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'tenants';
|
||||
value: string | Tenant;
|
||||
value: number | Tenant;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
value: number | Category;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
value: number | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'collection-level-config';
|
||||
value: string | CollectionLevelConfig;
|
||||
value: number | CollectionLevelConfig;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -912,10 +926,10 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -935,7 +949,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
@@ -1475,7 +1489,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
* via the `definition` "header".
|
||||
*/
|
||||
export interface Header {
|
||||
id: string;
|
||||
id: number;
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
@@ -1484,11 +1498,11 @@ export interface Header {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
@@ -1508,7 +1522,7 @@ export interface Header {
|
||||
* via the `definition` "footer".
|
||||
*/
|
||||
export interface Footer {
|
||||
id: string;
|
||||
id: number;
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
@@ -1517,11 +1531,11 @@ export interface Footer {
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null);
|
||||
url?: string | null;
|
||||
label: string;
|
||||
|
||||
@@ -36,7 +36,9 @@ export async function generateStaticParams() {
|
||||
|
||||
try {
|
||||
const pages = await getDocs<Page>('pages')
|
||||
return pages?.map(({ slug }) => slug)
|
||||
return pages?.map((page) => {
|
||||
return { slug: page.slug }
|
||||
})
|
||||
} catch (_err) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ export async function generateStaticParams() {
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'false'
|
||||
try {
|
||||
const ssrPosts = await getDocs<Post>(postsSlug)
|
||||
return ssrPosts?.map(({ slug }) => slug)
|
||||
return ssrPosts?.map((page) => {
|
||||
return { slug: page.slug }
|
||||
})
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ export async function generateStaticParams() {
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'false'
|
||||
try {
|
||||
const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug)
|
||||
return ssrPages?.map(({ slug }) => slug)
|
||||
return ssrPages?.map((page) => {
|
||||
return { slug: page.slug }
|
||||
})
|
||||
} catch (_err) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ export async function generateStaticParams() {
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'false'
|
||||
try {
|
||||
const ssrPages = await getDocs<Page>(ssrPagesSlug)
|
||||
return ssrPages?.map(({ slug }) => slug)
|
||||
return ssrPages?.map((page) => {
|
||||
return { slug: page.slug }
|
||||
})
|
||||
} catch (_err) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const RelationshipsBlock: React.FC<RelationshipsBlockProps> = (props) =>
|
||||
<b>Rich Text — Lexical:</b>
|
||||
</p>
|
||||
{data?.richTextLexical && (
|
||||
<RichText content={data.richTextLexical} renderUploadFilenameOnly serializer="lexical" />
|
||||
<RichText content={data.richTextLexical} renderUploadFilenameOnly />
|
||||
)}
|
||||
<p>
|
||||
<b>Upload:</b>
|
||||
|
||||
@@ -8,12 +8,12 @@ const RichText: React.FC<{
|
||||
className?: string
|
||||
content: any
|
||||
renderUploadFilenameOnly?: boolean
|
||||
serializer?: 'lexical' | 'slate'
|
||||
}> = ({ className, content, renderUploadFilenameOnly, serializer = 'slate' }) => {
|
||||
}> = ({ className, content, renderUploadFilenameOnly }) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
const serializer = Array.isArray(content) ? 'slate' : 'lexical'
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serializer === 'slate'
|
||||
|
||||
@@ -176,20 +176,44 @@ export const home: Omit<Page, 'createdAt' | 'id' | 'updatedAt'> = {
|
||||
arrayOfRelationships: [
|
||||
{
|
||||
uploadInArray: '{{MEDIA_ID}}',
|
||||
richTextInArray: [
|
||||
{
|
||||
richTextInArray: {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
text: ' ',
|
||||
format: '',
|
||||
type: 'relationship',
|
||||
version: 1,
|
||||
relationTo: postsSlug,
|
||||
value: {
|
||||
id: '{{POST_1_ID}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'upload',
|
||||
version: 1,
|
||||
fields: null,
|
||||
relationTo: 'media',
|
||||
value: {
|
||||
id: '{{MEDIA_ID}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
relationTo: postsSlug,
|
||||
type: 'relationship',
|
||||
value: {
|
||||
id: '{{POST_1_ID}}',
|
||||
},
|
||||
direction: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
relationshipInArrayMonoHasMany: ['{{POST_1_ID}}'],
|
||||
relationshipInArrayMonoHasOne: '{{POST_1_ID}}',
|
||||
relationshipInArrayPolyHasMany: [{ relationTo: 'posts', value: '{{POST_1_ID}}' }],
|
||||
|
||||
@@ -20,6 +20,20 @@ const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const seed: Config['onInit'] = async (payload) => {
|
||||
const existingUser = await payload.find({
|
||||
collection: 'users',
|
||||
where: {
|
||||
email: {
|
||||
equals: devUser.email,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Seed already ran => this is likely a consecutive, uncached getPayload call
|
||||
if (existingUser.docs.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const uploadsDir = path.resolve(dirname, './media')
|
||||
removeFiles(path.normalize(uploadsDir))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user