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

View File

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

View File

@@ -1,26 +1,58 @@
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical' 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' 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({ export async function convertLexicalToHTML({
converters, converters,
data, data,
payload, payload,
}: { req,
converters: HTMLConverter[] }: ConvertLexicalToHTMLArgs): Promise<string> {
data: SerializedEditorState
/**
* When the converter is called, payload CAN be passed in depending on where it's run.
*/
payload: Payload | null
}): Promise<string> {
if (data?.root?.children?.length) { if (data?.root?.children?.length) {
if (req === undefined && payload) {
req = await createLocalReq({}, payload)
}
return await convertLexicalNodesToHTML({ return await convertLexicalNodesToHTML({
converters, converters,
lexicalNodes: data?.root?.children, lexicalNodes: data?.root?.children,
parent: data?.root, parent: data?.root,
payload, req,
}) })
} }
return '' return ''
@@ -30,15 +62,15 @@ export async function convertLexicalNodesToHTML({
converters, converters,
lexicalNodes, lexicalNodes,
parent, parent,
payload, req,
}: { }: {
converters: HTMLConverter[] converters: HTMLConverter[]
lexicalNodes: SerializedLexicalNode[] lexicalNodes: SerializedLexicalNode[]
parent: SerializedLexicalNodeWithParent 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> { }): Promise<string> {
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown')) const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
@@ -55,7 +87,7 @@ export async function convertLexicalNodesToHTML({
converters, converters,
node, node,
parent, parent,
payload, req,
}) })
} }
return '<span>unknown node</span>' return '<span>unknown node</span>'
@@ -65,7 +97,7 @@ export async function convertLexicalNodesToHTML({
converters, converters,
node, node,
parent, parent,
payload, req,
}) })
} catch (error) { } catch (error) {
console.error('Error converting lexical node to HTML:', error, 'node:', node) console.error('Error converting lexical node to HTML:', error, 'node:', node)

View File

@@ -1,5 +1,5 @@
import type { SerializedLexicalNode } from 'lexical' import type { SerializedLexicalNode } from 'lexical'
import type { Payload } from 'payload' import type { Payload, PayloadRequest } from 'payload'
export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = { export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
converter: ({ converter: ({
@@ -7,16 +7,16 @@ export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNod
converters, converters,
node, node,
parent, parent,
payload, req,
}: { }: {
childIndex: number childIndex: number
converters: HTMLConverter[] converters: HTMLConverter[]
node: T node: T
parent: SerializedLexicalNodeWithParent 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 }) => Promise<string> | string
nodeTypes: string[] nodeTypes: string[]
} }

View File

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

View File

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

View File

@@ -128,7 +128,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, payload }) => { converter: async ({ converters, node, parent, req }) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
lexicalNodes: node.children, lexicalNodes: node.children,
@@ -136,7 +136,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
...node, ...node,
parent, parent,
}, },
payload, req,
}) })
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : '' const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
@@ -162,7 +162,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, payload }) => { converter: async ({ converters, node, parent, req }) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
lexicalNodes: node.children, lexicalNodes: node.children,
@@ -170,7 +170,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
...node, ...node,
parent, parent,
}, },
payload, req,
}) })
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : '' 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' import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
export const ListHTMLConverter: HTMLConverter<SerializedListNode> = { export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
converter: async ({ converters, node, parent, payload }) => { converter: async ({ converters, node, parent, req }) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
lexicalNodes: node.children, lexicalNodes: node.children,
@@ -16,7 +16,7 @@ export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
...node, ...node,
parent, parent,
}, },
payload, req,
}) })
return `<${node?.tag} class="list-${node?.listType}">${childrenText}</${node?.tag}>` return `<${node?.tag} class="list-${node?.listType}">${childrenText}</${node?.tag}>`
@@ -25,7 +25,7 @@ export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
} }
export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = { 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 hasSubLists = node.children.some((child) => child.type === 'list')
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
@@ -35,7 +35,7 @@ export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = {
...node, ...node,
parent, parent,
}, },
payload, req,
}) })
if ('listType' in parent && parent?.listType === 'check') { if ('listType' in parent && parent?.listType === 'check') {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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