feat(richtext-lexical): make req available to html converters, use req.dataLoader instead of payload.findByID for upload node population (#6858)

This commit is contained in:
Alessio Gravili
2024-06-19 16:01:18 -04:00
committed by GitHub
parent b5ac0bd365
commit 285835f23a
14 changed files with 114 additions and 56 deletions

View File

@@ -21,7 +21,7 @@ export const BlockquoteFeature: FeatureProviderProviderServer<undefined, undefin
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, payload }) => {
converter: async ({ converters, node, parent, req }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
@@ -29,7 +29,7 @@ export const BlockquoteFeature: FeatureProviderProviderServer<undefined, undefin
...node,
parent,
},
payload,
req,
})
return `<blockquote>${childrenText}</blockquote>`

View File

@@ -5,7 +5,7 @@ import type { HTMLConverter } from '../types.js'
import { convertLexicalNodesToHTML } from '../index.js'
export const ParagraphHTMLConverter: HTMLConverter<SerializedParagraphNode> = {
async converter({ converters, node, parent, payload }) {
async converter({ converters, node, parent, req }) {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
@@ -13,7 +13,7 @@ export const ParagraphHTMLConverter: HTMLConverter<SerializedParagraphNode> = {
...node,
parent,
},
payload,
req,
})
return `<p>${childrenText}</p>`
},

View File

@@ -1,26 +1,58 @@
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
import type { Payload } from 'payload'
import type { Payload, PayloadRequest } from 'payload'
import { createLocalReq } from 'payload'
import type { HTMLConverter, SerializedLexicalNodeWithParent } from './types.js'
export type ConvertLexicalToHTMLArgs = {
converters: HTMLConverter[]
data: SerializedEditorState
} & (
| {
/**
* This payload property will only be used if req is undefined.
*/
payload?: Payload
/**
* When the converter is called, req CAN be passed in depending on where it's run.
* If this is undefined and config is passed through, lexical will create a new req object for you. If this is null or
* config is undefined, lexical will not create a new req object for you and local API / server-side-only
* functionality will be disabled.
*/
req?: null | undefined
}
| {
/**
* This payload property will only be used if req is undefined.
*/
payload?: never
/**
* When the converter is called, req CAN be passed in depending on where it's run.
* If this is undefined and config is passed through, lexical will create a new req object for you. If this is null or
* config is undefined, lexical will not create a new req object for you and local API / server-side-only
* functionality will be disabled.
*/
req: PayloadRequest
}
)
export async function convertLexicalToHTML({
converters,
data,
payload,
}: {
converters: HTMLConverter[]
data: SerializedEditorState
/**
* When the converter is called, payload CAN be passed in depending on where it's run.
*/
payload: Payload | null
}): Promise<string> {
req,
}: ConvertLexicalToHTMLArgs): Promise<string> {
if (data?.root?.children?.length) {
if (req === undefined && payload) {
req = await createLocalReq({}, payload)
}
return await convertLexicalNodesToHTML({
converters,
lexicalNodes: data?.root?.children,
parent: data?.root,
payload,
req,
})
}
return ''
@@ -30,15 +62,15 @@ export async function convertLexicalNodesToHTML({
converters,
lexicalNodes,
parent,
payload,
req,
}: {
converters: HTMLConverter[]
lexicalNodes: SerializedLexicalNode[]
parent: SerializedLexicalNodeWithParent
/**
* When the converter is called, payload CAN be passed in depending on where it's run.
* When the converter is called, req CAN be passed in depending on where it's run.
*/
payload: Payload | null
req: PayloadRequest | null
}): Promise<string> {
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
@@ -55,7 +87,7 @@ export async function convertLexicalNodesToHTML({
converters,
node,
parent,
payload,
req,
})
}
return '<span>unknown node</span>'
@@ -65,7 +97,7 @@ export async function convertLexicalNodesToHTML({
converters,
node,
parent,
payload,
req,
})
} catch (error) {
console.error('Error converting lexical node to HTML:', error, 'node:', node)

View File

@@ -1,5 +1,5 @@
import type { SerializedLexicalNode } from 'lexical'
import type { Payload } from 'payload'
import type { Payload, PayloadRequest } from 'payload'
export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
converter: ({
@@ -7,16 +7,16 @@ export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNod
converters,
node,
parent,
payload,
req,
}: {
childIndex: number
converters: HTMLConverter[]
node: T
parent: SerializedLexicalNodeWithParent
/**
* When the converter is called, payload CAN be passed in depending on where it's run.
* When the converter is called, req CAN be passed in depending on where it's run.
*/
payload: Payload | null
req: PayloadRequest | null
}) => Promise<string> | string
nodeTypes: string[]
}

View File

@@ -209,7 +209,7 @@ export const lexicalHTML: (
return await convertLexicalToHTML({
converters: finalConverters,
data: lexicalFieldData,
payload: req.payload,
req,
})
},
],

View File

@@ -36,7 +36,7 @@ export const HeadingFeature: FeatureProviderProviderServer<
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, payload }) => {
converter: async ({ converters, node, parent, req }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
@@ -44,7 +44,7 @@ export const HeadingFeature: FeatureProviderProviderServer<
...node,
parent,
},
payload,
req,
})
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'

View File

@@ -128,7 +128,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, payload }) => {
converter: async ({ converters, node, parent, req }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
@@ -136,7 +136,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
...node,
parent,
},
payload,
req,
})
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
@@ -162,7 +162,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, payload }) => {
converter: async ({ converters, node, parent, req }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
@@ -170,7 +170,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
...node,
parent,
},
payload,
req,
})
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''

View File

@@ -8,7 +8,7 @@ import type { HTMLConverter } from '../converters/html/converter/types.js'
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
converter: async ({ converters, node, parent, payload }) => {
converter: async ({ converters, node, parent, req }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
@@ -16,7 +16,7 @@ export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
...node,
parent,
},
payload,
req,
})
return `<${node?.tag} class="list-${node?.listType}">${childrenText}</${node?.tag}>`
@@ -25,7 +25,7 @@ export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
}
export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = {
converter: async ({ converters, node, parent, payload }) => {
converter: async ({ converters, node, parent, req }) => {
const hasSubLists = node.children.some((child) => child.type === 'list')
const childrenText = await convertLexicalNodesToHTML({
@@ -35,7 +35,7 @@ export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = {
...node,
parent,
},
payload,
req,
})
if ('listType' in parent && parent?.listType === 'check') {

View File

@@ -84,7 +84,7 @@ export const RelationshipFeature: FeatureProviderProviderServer<
populationPromises.push(
populate({
id,
collection,
collectionSlug: collection.config.slug,
currentDepth,
data: node,
depth: populateDepth,

View File

@@ -30,7 +30,7 @@ export const relationshipPopulationPromiseHOC = (
populationPromises.push(
populate({
id,
collection,
collectionSlug: collection.config.slug,
currentDepth,
data: node,
depth: populateDepth,

View File

@@ -101,18 +101,28 @@ export const UploadFeature: FeatureProviderProviderServer<
createNode({
converters: {
html: {
converter: async ({ node, payload }) => {
converter: async ({ node, req }) => {
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
if (payload) {
let uploadDocument: TypeWithID & FileData
if (req?.payload) {
const uploadDocument: {
value?: TypeWithID & FileData
} = {}
try {
uploadDocument = (await payload.findByID({
await populate({
id,
collection: node.relationTo,
})) as TypeWithID & FileData
collectionSlug: node.relationTo,
currentDepth: 0,
data: uploadDocument,
depth: 1,
draft: false,
key: 'value',
overrideAccess: false,
req,
showHiddenFields: false,
})
} catch (ignored) {
// eslint-disable-next-line no-console
console.error(
@@ -124,20 +134,23 @@ export const UploadFeature: FeatureProviderProviderServer<
return `<img />`
}
const url = getAbsoluteURL(uploadDocument?.url, payload)
const url = getAbsoluteURL(uploadDocument?.value?.url, req?.payload)
/**
* If the upload is not an image, return a link to the upload
*/
if (!uploadDocument?.mimeType?.startsWith('image')) {
return `<a href="${url}" rel="noopener noreferrer">${uploadDocument.filename}</a>`
if (!uploadDocument?.value?.mimeType?.startsWith('image')) {
return `<a href="${url}" rel="noopener noreferrer">${uploadDocument.value?.filename}</a>`
}
/**
* If the upload is a simple image with no different sizes, return a simple img tag
*/
if (!uploadDocument?.sizes || !Object.keys(uploadDocument?.sizes).length) {
return `<img src="${url}" alt="${uploadDocument?.filename}" width="${uploadDocument?.width}" height="${uploadDocument?.height}"/>`
if (
!uploadDocument?.value?.sizes ||
!Object.keys(uploadDocument?.value?.sizes).length
) {
return `<img src="${url}" alt="${uploadDocument?.value?.filename}" width="${uploadDocument?.value?.width}" height="${uploadDocument?.value?.height}"/>`
}
/**
@@ -146,10 +159,10 @@ export const UploadFeature: FeatureProviderProviderServer<
let pictureHTML = '<picture>'
// Iterate through each size in the data.sizes object
for (const size in uploadDocument.sizes) {
for (const size in uploadDocument.value?.sizes) {
const imageSize: FileSize & {
url?: string
} = uploadDocument.sizes[size]
} = uploadDocument.value?.sizes[size]
// Skip if any property of the size object is null
if (
@@ -162,13 +175,13 @@ export const UploadFeature: FeatureProviderProviderServer<
) {
continue
}
const imageSizeURL = getAbsoluteURL(imageSize?.url, payload)
const imageSizeURL = getAbsoluteURL(imageSize?.url, req?.payload)
pictureHTML += `<source srcset="${imageSizeURL}" media="(max-width: ${imageSize.width}px)" type="${imageSize.mimeType}">`
}
// Add the default img tag
pictureHTML += `<img src="${url}" alt="Image" width="${uploadDocument.width}" height="${uploadDocument.height}">`
pictureHTML += `<img src="${url}" alt="Image" width="${uploadDocument.value?.width}" height="${uploadDocument.value?.height}">`
pictureHTML += '</picture>'
return pictureHTML
} else {
@@ -228,7 +241,7 @@ export const UploadFeature: FeatureProviderProviderServer<
populationPromises.push(
populate({
id,
collection,
collectionSlug: collection.config.slug,
currentDepth,
data: node,
depth: populateDepth,

View File

@@ -36,7 +36,7 @@ export const uploadPopulationPromiseHOC = (
populationPromises.push(
populate({
id,
collection,
collectionSlug: collection.config.slug,
currentDepth,
data: node,
depth: populateDepth,

View File

@@ -15,7 +15,7 @@ type Arguments = {
export const populate = async ({
id,
collection,
collectionSlug,
currentDepth,
data,
depth,
@@ -25,7 +25,7 @@ export const populate = async ({
req,
showHiddenFields,
}: Arguments & {
collection: Collection
collectionSlug: string
id: number | string
}): Promise<void> => {
const shouldPopulate = depth && currentDepth <= depth
@@ -38,7 +38,7 @@ export const populate = async ({
const doc = await req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: collection.config.slug,
collectionSlug,
currentDepth: currentDepth + 1,
depth,
docID: id as string,

View File

@@ -672,6 +672,8 @@ export interface TextField {
text: string;
localizedText?: string | null;
i18nText?: string | null;
defaultString?: string | null;
defaultEmptyString?: string | null;
defaultFunction?: string | null;
defaultAsync?: string | null;
overrideLength?: string | null;
@@ -915,6 +917,17 @@ export interface JsonField {
| number
| boolean
| null;
group?: {
jsonWithinGroup?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
};
updatedAt: string;
createdAt: string;
}