fix(richtext-lexical)!: html converters not respecting overrideAccess property when populating values, in local API

This commit is contained in:
Alessio Gravili
2024-08-06 11:15:09 -04:00
parent a422a0d568
commit c15d679b65
14 changed files with 156 additions and 27 deletions

View File

@@ -7,7 +7,6 @@ import type { CSSProperties } from 'react'
import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode) import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode)
import type { JSONSchema4 } from 'json-schema' import type { JSONSchema4 } from 'json-schema'
import type React from 'react' import type React from 'react'
import type { DeepPartial } from 'ts-essentials'
import type { RichTextAdapter, RichTextAdapterProvider } from '../../admin/RichText.js' import type { RichTextAdapter, RichTextAdapterProvider } from '../../admin/RichText.js'
import type { ErrorComponent } from '../../admin/forms/Error.js' import type { ErrorComponent } from '../../admin/forms/Error.js'
@@ -33,6 +32,10 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
context: RequestContext context: RequestContext
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */ /** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
data?: Partial<TData> data?: Partial<TData>
/**
* Only available in the `afterRead` hook.
*/
draft?: boolean
/** The field which the hook is running against. */ /** The field which the hook is running against. */
field: FieldAffectingData field: FieldAffectingData
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */ /** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
@@ -60,6 +63,10 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
* The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data. * The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data.
*/ */
schemaPath: string[] schemaPath: string[]
/**
* Only available in the `afterRead` hook.
*/
showHiddenFields?: boolean
/** The sibling data passed to a field that the hook is running against. */ /** The sibling data passed to a field that the hook is running against. */
siblingData: Partial<TSiblingData> siblingData: Partial<TSiblingData>
/** /**

View File

@@ -205,6 +205,7 @@ export const promise = async ({
collection, collection,
context, context,
data: doc, data: doc,
draft,
field, field,
findMany, findMany,
global, global,
@@ -214,6 +215,7 @@ export const promise = async ({
path: fieldPath, path: fieldPath,
req, req,
schemaPath: fieldSchemaPath, schemaPath: fieldSchemaPath,
showHiddenFields,
siblingData: siblingDoc, siblingData: siblingDoc,
value, value,
}) })
@@ -230,6 +232,7 @@ export const promise = async ({
collection, collection,
context, context,
data: doc, data: doc,
draft,
field, field,
findMany, findMany,
global, global,
@@ -239,6 +242,7 @@ export const promise = async ({
path: fieldPath, path: fieldPath,
req, req,
schemaPath: fieldSchemaPath, schemaPath: fieldSchemaPath,
showHiddenFields,
siblingData: siblingDoc, siblingData: siblingDoc,
value: siblingDoc[field.name], value: siblingDoc[field.name],
}) })

View File

@@ -28,15 +28,26 @@ export const BlockquoteFeature = createServerFeature({
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, req }) => { converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
return `<blockquote>${childrenText}</blockquote>` return `<blockquote>${childrenText}</blockquote>`

View File

@@ -5,15 +5,18 @@ 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, req }) { async converter({ converters, draft, node, overrideAccess, parent, req, showHiddenFields }) {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
return `<p>${childrenText}</p>` return `<p>${childrenText}</p>`
}, },

View File

@@ -8,6 +8,9 @@ import type { HTMLConverter, SerializedLexicalNodeWithParent } from './types.js'
export type ConvertLexicalToHTMLArgs = { export type ConvertLexicalToHTMLArgs = {
converters: HTMLConverter[] converters: HTMLConverter[]
data: SerializedEditorState data: SerializedEditorState
draft?: boolean // default false
overrideAccess?: boolean // default false
showHiddenFields?: boolean // default false
} & ( } & (
| { | {
/** /**
@@ -40,8 +43,11 @@ export type ConvertLexicalToHTMLArgs = {
export async function convertLexicalToHTML({ export async function convertLexicalToHTML({
converters, converters,
data, data,
draft,
overrideAccess,
payload, payload,
req, req,
showHiddenFields,
}: ConvertLexicalToHTMLArgs): Promise<string> { }: ConvertLexicalToHTMLArgs): Promise<string> {
if (data?.root?.children?.length) { if (data?.root?.children?.length) {
if (req === undefined && payload) { if (req === undefined && payload) {
@@ -50,9 +56,12 @@ export async function convertLexicalToHTML({
return await convertLexicalNodesToHTML({ return await convertLexicalNodesToHTML({
converters, converters,
draft: draft === undefined ? false : draft,
lexicalNodes: data?.root?.children, lexicalNodes: data?.root?.children,
overrideAccess: overrideAccess === undefined ? false : overrideAccess,
parent: data?.root, parent: data?.root,
req, req,
showHiddenFields: showHiddenFields === undefined ? false : showHiddenFields,
}) })
} }
return '' return ''
@@ -60,17 +69,23 @@ export async function convertLexicalToHTML({
export async function convertLexicalNodesToHTML({ export async function convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes, lexicalNodes,
overrideAccess,
parent, parent,
req, req,
showHiddenFields,
}: { }: {
converters: HTMLConverter[] converters: HTMLConverter[]
draft: boolean
lexicalNodes: SerializedLexicalNode[] lexicalNodes: SerializedLexicalNode[]
overrideAccess: boolean
parent: SerializedLexicalNodeWithParent parent: SerializedLexicalNodeWithParent
/** /**
* When the converter is called, req 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.
*/ */
req: PayloadRequest | null req: PayloadRequest | null
showHiddenFields: boolean
}): Promise<string> { }): Promise<string> {
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown')) const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
@@ -85,9 +100,12 @@ export async function convertLexicalNodesToHTML({
return await unknownConverter.converter({ return await unknownConverter.converter({
childIndex: i, childIndex: i,
converters, converters,
draft,
node, node,
overrideAccess,
parent, parent,
req, req,
showHiddenFields,
}) })
} }
return '<span>unknown node</span>' return '<span>unknown node</span>'
@@ -95,9 +113,12 @@ export async function convertLexicalNodesToHTML({
return await converterForNode.converter({ return await converterForNode.converter({
childIndex: i, childIndex: i,
converters, converters,
draft,
node, node,
overrideAccess,
parent, parent,
req, req,
showHiddenFields,
}) })
} 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,22 +1,19 @@
import type { SerializedLexicalNode } from 'lexical' import type { SerializedLexicalNode } from 'lexical'
import type { Payload, PayloadRequest } from 'payload' import type { PayloadRequest } from 'payload'
export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = { export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
converter: ({ converter: (args: {
childIndex,
converters,
node,
parent,
req,
}: {
childIndex: number childIndex: number
converters: HTMLConverter[] converters: HTMLConverter<any>[]
draft: boolean
node: T node: T
overrideAccess: boolean
parent: SerializedLexicalNodeWithParent parent: SerializedLexicalNodeWithParent
/** /**
* When the converter is called, req 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.
*/ */
req: PayloadRequest | null req: PayloadRequest | null
showHiddenFields: boolean
}) => Promise<string> | string }) => Promise<string> | string
nodeTypes: string[] nodeTypes: string[]
} }

View File

@@ -4,8 +4,8 @@ import { createServerFeature } from '../../../utilities/createServerFeature.js'
export type HTMLConverterFeatureProps = { export type HTMLConverterFeatureProps = {
converters?: converters?:
| (({ defaultConverters }: { defaultConverters: HTMLConverter[] }) => HTMLConverter[]) | (({ defaultConverters }: { defaultConverters: HTMLConverter<any>[] }) => HTMLConverter<any>[])
| HTMLConverter[] | HTMLConverter<any>[]
} }
// This is just used to save the props on the richText field // This is just used to save the props on the richText field

View File

@@ -160,7 +160,16 @@ export const lexicalHTML: (
}, },
hooks: { hooks: {
afterRead: [ afterRead: [
async ({ collection, field, global, req, siblingData }) => { async ({
collection,
draft,
field,
global,
overrideAccess,
req,
showHiddenFields,
siblingData,
}) => {
const fields = collection ? collection.fields : global.fields const fields = collection ? collection.fields : global.fields
const foundSiblingFields = findFieldPathAndSiblingFields(fields, [], field) const foundSiblingFields = findFieldPathAndSiblingFields(fields, [], field)
@@ -209,7 +218,10 @@ export const lexicalHTML: (
return await convertLexicalToHTML({ return await convertLexicalToHTML({
converters: finalConverters, converters: finalConverters,
data: lexicalFieldData, data: lexicalFieldData,
draft,
overrideAccess,
req, req,
showHiddenFields,
}) })
}, },
], ],

View File

@@ -46,15 +46,26 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, req }) => { converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
return `<table class="lexical-table" style="border-collapse: collapse;">${childrenText}</table>` return `<table class="lexical-table" style="border-collapse: collapse;">${childrenText}</table>`
}, },
@@ -66,15 +77,26 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, req }) => { converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
const tagName = node.headerState > 0 ? 'th' : 'td' const tagName = node.headerState > 0 ? 'th' : 'td'
@@ -95,15 +117,26 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, req }) => { converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
return `<tr class="lexical-table-row">${childrenText}</tr>` return `<tr class="lexical-table-row">${childrenText}</tr>`
}, },

View File

@@ -46,15 +46,26 @@ export const HeadingFeature = createServerFeature<
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, req }) => { converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>' return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'

View File

@@ -116,15 +116,26 @@ export const LinkFeature = createServerFeature<
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, req }) => { converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : '' const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
@@ -150,15 +161,26 @@ export const LinkFeature = createServerFeature<
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, req }) => { converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : '' const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''

View File

@@ -7,15 +7,18 @@ import type { SerializedListItemNode, SerializedListNode } from './plugin/index.
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, req }) => { converter: async ({ converters, draft, node, overrideAccess, parent, req, showHiddenFields }) => {
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
return `<${node?.tag} class="list-${node?.listType}">${childrenText}</${node?.tag}>` return `<${node?.tag} class="list-${node?.listType}">${childrenText}</${node?.tag}>`
@@ -24,17 +27,20 @@ export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
} }
export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = { export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = {
converter: async ({ converters, node, parent, req }) => { converter: async ({ converters, draft, node, overrideAccess, parent, req, showHiddenFields }) => {
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({
converters, converters,
draft,
lexicalNodes: node.children, lexicalNodes: node.children,
overrideAccess,
parent: { parent: {
...node, ...node,
parent, parent,
}, },
req, req,
showHiddenFields,
}) })
if ('listType' in parent && parent?.listType === 'check') { if ('listType' in parent && parent?.listType === 'check') {

View File

@@ -99,7 +99,7 @@ export const UploadFeature = createServerFeature<
createNode({ createNode({
converters: { converters: {
html: { html: {
converter: async ({ node, req }) => { converter: async ({ draft, node, overrideAccess, req, showHiddenFields }) => {
// @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
@@ -115,11 +115,11 @@ export const UploadFeature = createServerFeature<
currentDepth: 0, currentDepth: 0,
data: uploadDocument, data: uploadDocument,
depth: 1, depth: 1,
draft: false, draft,
key: 'value', key: 'value',
overrideAccess: false, overrideAccess,
req, req,
showHiddenFields: false, showHiddenFields,
}) })
} catch (ignored) { } catch (ignored) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@@ -74,6 +74,7 @@ export interface Config {
export interface UserAuthOperations { export interface UserAuthOperations {
forgotPassword: { forgotPassword: {
email: string; email: string;
password: string;
}; };
login: { login: {
email: string; email: string;
@@ -85,6 +86,7 @@ export interface UserAuthOperations {
}; };
unlock: { unlock: {
email: string; email: string;
password: string;
}; };
} }
/** /**