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 type { JSONSchema4 } from 'json-schema'
import type React from 'react'
import type { DeepPartial } from 'ts-essentials'
import type { RichTextAdapter, RichTextAdapterProvider } from '../../admin/RichText.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
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
data?: Partial<TData>
/**
* Only available in the `afterRead` hook.
*/
draft?: boolean
/** The field which the hook is running against. */
field: FieldAffectingData
/** 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.
*/
schemaPath: string[]
/**
* Only available in the `afterRead` hook.
*/
showHiddenFields?: boolean
/** The sibling data passed to a field that the hook is running against. */
siblingData: Partial<TSiblingData>
/**

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,9 @@ import type { HTMLConverter, SerializedLexicalNodeWithParent } from './types.js'
export type ConvertLexicalToHTMLArgs = {
converters: HTMLConverter[]
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({
converters,
data,
draft,
overrideAccess,
payload,
req,
showHiddenFields,
}: ConvertLexicalToHTMLArgs): Promise<string> {
if (data?.root?.children?.length) {
if (req === undefined && payload) {
@@ -50,9 +56,12 @@ export async function convertLexicalToHTML({
return await convertLexicalNodesToHTML({
converters,
draft: draft === undefined ? false : draft,
lexicalNodes: data?.root?.children,
overrideAccess: overrideAccess === undefined ? false : overrideAccess,
parent: data?.root,
req,
showHiddenFields: showHiddenFields === undefined ? false : showHiddenFields,
})
}
return ''
@@ -60,17 +69,23 @@ export async function convertLexicalToHTML({
export async function convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes,
overrideAccess,
parent,
req,
showHiddenFields,
}: {
converters: HTMLConverter[]
draft: boolean
lexicalNodes: SerializedLexicalNode[]
overrideAccess: boolean
parent: SerializedLexicalNodeWithParent
/**
* When the converter is called, req CAN be passed in depending on where it's run.
*/
req: PayloadRequest | null
showHiddenFields: boolean
}): Promise<string> {
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
@@ -85,9 +100,12 @@ export async function convertLexicalNodesToHTML({
return await unknownConverter.converter({
childIndex: i,
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
})
}
return '<span>unknown node</span>'
@@ -95,9 +113,12 @@ export async function convertLexicalNodesToHTML({
return await converterForNode.converter({
childIndex: i,
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
})
} catch (error) {
console.error('Error converting lexical node to HTML:', error, 'node:', node)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -116,15 +116,26 @@ export const LinkFeature = createServerFeature<
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, req }) => {
converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
@@ -150,15 +161,26 @@ export const LinkFeature = createServerFeature<
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, req }) => {
converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
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'
export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
converter: async ({ converters, node, parent, req }) => {
converter: async ({ converters, draft, node, overrideAccess, parent, req, showHiddenFields }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
return `<${node?.tag} class="list-${node?.listType}">${childrenText}</${node?.tag}>`
@@ -24,17 +27,20 @@ export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
}
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 childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
if ('listType' in parent && parent?.listType === 'check') {

View File

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

View File

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