fix(richtext-lexical): re-use payload population logic to fix population-related issues (#4291)

* chore(richtext-lexical): Add int test which reproduces the issue

* chore: Remove unnecessary await in core afterRead promise

* fix(richtext-lexical): re-use recurseNestedFields from payload instead of using own recurseNestedFields

* chore(richtext-lexical): pass in missing properties which are available in the core afterRead hook

* chore: remove unnecessary block
This commit is contained in:
Alessio Gravili
2023-11-28 19:18:07 +01:00
committed by GitHub
parent 1fe4f4c5f4
commit 094d02ce1d
18 changed files with 284 additions and 223 deletions

View File

@@ -1,6 +1,7 @@
import type { JSONSchema4 } from 'json-schema' import type { JSONSchema4 } from 'json-schema'
import type { PayloadRequest } from '../../../../../express/types' import type { PayloadRequest } from '../../../../../express/types'
import type { RequestContext } from '../../../../../express/types'
import type { RichTextField, Validate } from '../../../../../fields/config/types' import type { RichTextField, Validate } from '../../../../../fields/config/types'
import type { CellComponentProps } from '../../../views/collections/List/Cell/types' import type { CellComponentProps } from '../../../views/collections/List/Cell/types'
@@ -39,10 +40,14 @@ export type RichTextAdapter<
isRequired: boolean isRequired: boolean
}) => JSONSchema4 }) => JSONSchema4
populationPromise?: (data: { populationPromise?: (data: {
context: RequestContext
currentDepth?: number currentDepth?: number
depth: number depth: number
field: RichTextField<Value, AdapterProps, ExtraFieldProperties> field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
findMany: boolean
flattenLocales: boolean
overrideAccess?: boolean overrideAccess?: boolean
populationPromises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>

View File

@@ -1,21 +1,24 @@
export { withMergedProps } from '../admin/components/utilities/WithMergedProps' export { withMergedProps } from '../admin/components/utilities/WithMergedProps'
export { extractTranslations } from '../translations/extractTranslations' export { promise as afterReadPromise } from '../fields/hooks/afterRead/promise'
export { traverseFields as afterReadTraverseFields } from '../fields/hooks/afterRead/traverseFields'
export { extractTranslations } from '../translations/extractTranslations'
export { i18nInit } from '../translations/init' export { i18nInit } from '../translations/init'
export { combineMerge } from '../utilities/combineMerge' export { combineMerge } from '../utilities/combineMerge'
export { export {
configToJSONSchema, configToJSONSchema,
entityToJSONSchema, entityToJSONSchema,
withNullableJSONSchemaType, withNullableJSONSchemaType,
} from '../utilities/configToJSONSchema' } from '../utilities/configToJSONSchema'
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated' export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
export { deepCopyObject } from '../utilities/deepCopyObject'
export { deepCopyObject } from '../utilities/deepCopyObject'
export { deepMerge } from '../utilities/deepMerge' export { deepMerge } from '../utilities/deepMerge'
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON' export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON'
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields' export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields'
export { formatLabels, formatNames, toWords } from '../utilities/formatLabels' export { formatLabels, formatNames, toWords } from '../utilities/formatLabels'
export { getIDType } from '../utilities/getIDType' export { getIDType } from '../utilities/getIDType'
export { getTranslation } from '../utilities/getTranslation' export { getTranslation } from '../utilities/getTranslation'
export { isValidID } from '../utilities/isValidID' export { isValidID } from '../utilities/isValidID'

View File

@@ -25,12 +25,14 @@ type Args = {
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>
triggerAccessControl?: boolean
triggerHooks?: boolean
} }
// This function is responsible for the following actions, in order: // This function is responsible for the following actions, in order:
// - Remove hidden fields from response // - Remove hidden fields from response
// - Flatten locales into requested locale // - Flatten locales into requested locale
// - Sanitize outgoing data (point field, etc) // - Sanitize outgoing data (point field, etc.)
// - Execute field hooks // - Execute field hooks
// - Execute read access control // - Execute read access control
// - Populate relationships // - Populate relationships
@@ -51,6 +53,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
triggerAccessControl = true,
triggerHooks = true,
}: Args): Promise<void> => { }: Args): Promise<void> => {
if ( if (
fieldAffectsData(field) && fieldAffectsData(field) &&
@@ -138,10 +142,14 @@ export const promise = async ({
// This is run here AND in the GraphQL Resolver // This is run here AND in the GraphQL Resolver
if (editor?.populationPromise) { if (editor?.populationPromise) {
const populationPromise = editor.populationPromise({ const populationPromise = editor.populationPromise({
context,
currentDepth, currentDepth,
depth, depth,
field, field,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
@@ -186,7 +194,7 @@ export const promise = async ({
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
// Execute hooks // Execute hooks
if (field.hooks?.afterRead) { if (triggerHooks && field.hooks?.afterRead) {
await field.hooks.afterRead.reduce(async (priorHook, currentHook) => { await field.hooks.afterRead.reduce(async (priorHook, currentHook) => {
await priorHook await priorHook
@@ -241,7 +249,7 @@ export const promise = async ({
} }
// Execute access control // Execute access control
if (field.access && field.access.read) { if (triggerAccessControl && field.access && field.access.read) {
const result = overrideAccess const result = overrideAccess
? true ? true
: await field.access.read({ : await field.access.read({
@@ -293,6 +301,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc: groupDoc, siblingDoc: groupDoc,
triggerAccessControl,
triggerHooks,
}) })
break break
@@ -319,6 +329,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc: row || {}, siblingDoc: row || {},
triggerAccessControl,
triggerHooks,
}) })
}) })
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) { } else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
@@ -341,6 +353,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc: row || {}, siblingDoc: row || {},
triggerAccessControl,
triggerHooks,
}) })
}) })
} }
@@ -375,6 +389,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc: row || {}, siblingDoc: row || {},
triggerAccessControl,
triggerHooks,
}) })
} }
}) })
@@ -401,6 +417,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc: row || {}, siblingDoc: row || {},
triggerAccessControl,
triggerHooks,
}) })
} }
}) })
@@ -431,6 +449,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
triggerAccessControl,
triggerHooks,
}) })
break break
@@ -443,7 +463,7 @@ export const promise = async ({
if (typeof siblingDoc[field.name] !== 'object') tabDoc = {} if (typeof siblingDoc[field.name] !== 'object') tabDoc = {}
} }
await traverseFields({ traverseFields({
collection, collection,
context, context,
currentDepth, currentDepth,
@@ -459,6 +479,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc: tabDoc, siblingDoc: tabDoc,
triggerAccessControl,
triggerHooks,
}) })
break break
@@ -481,6 +503,8 @@ export const promise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
triggerAccessControl,
triggerHooks,
}) })
break break
} }

View File

@@ -21,6 +21,8 @@ type Args = {
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>
triggerAccessControl?: boolean
triggerHooks?: boolean
} }
export const traverseFields = ({ export const traverseFields = ({
@@ -39,6 +41,8 @@ export const traverseFields = ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
triggerAccessControl = true,
triggerHooks = true,
}: Args): void => { }: Args): void => {
fields.forEach((field) => { fields.forEach((field) => {
fieldPromises.push( fieldPromises.push(
@@ -58,6 +62,8 @@ export const traverseFields = ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
triggerAccessControl,
triggerHooks,
}), }),
) )
}) })

View File

@@ -436,8 +436,13 @@ function buildObjectType({
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise. // Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
if (editor?.populationPromise) { if (editor?.populationPromise) {
await editor?.populationPromise({ await editor?.populationPromise({
context,
depth, depth,
field, field,
findMany: false,
flattenLocales: false,
overrideAccess: false,
populationPromises: [],
req: context.req, req: context.req,
showHiddenFields: false, showHiddenFields: false,
siblingDoc: parent, siblingDoc: parent,

View File

@@ -12,13 +12,18 @@ export const blockPopulationPromiseHOC = (
props: BlocksFeatureProps, props: BlocksFeatureProps,
): PopulationPromise<SerializedBlockNode> => { ): PopulationPromise<SerializedBlockNode> => {
const blockPopulationPromise: PopulationPromise<SerializedBlockNode> = ({ const blockPopulationPromise: PopulationPromise<SerializedBlockNode> = ({
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises,
findMany,
flattenLocales,
node, node,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const blocks: Block[] = props.blocks const blocks: Block[] = props.blocks
const blockFieldData = node.fields const blockFieldData = node.fields
@@ -43,10 +48,14 @@ export const blockPopulationPromiseHOC = (
} }
recurseNestedFields({ recurseNestedFields({
context,
currentDepth, currentDepth,
data: blockFieldData, data: blockFieldData,
depth, depth,
editorPopulationPromises,
fields: block.fields, fields: block.fields,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises, promises,

View File

@@ -9,14 +9,19 @@ export const linkPopulationPromiseHOC = (
props: LinkFeatureProps, props: LinkFeatureProps,
): PopulationPromise<SerializedLinkNode> => { ): PopulationPromise<SerializedLinkNode> => {
const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({ const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises,
field, field,
findMany,
flattenLocales,
node, node,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
@@ -42,10 +47,14 @@ export const linkPopulationPromiseHOC = (
} }
if (Array.isArray(props.fields)) { if (Array.isArray(props.fields)) {
recurseNestedFields({ recurseNestedFields({
context,
currentDepth, currentDepth,
data: node.fields || {}, data: node.fields || {},
depth, depth,
editorPopulationPromises,
fields: props.fields, fields: props.fields,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises, promises,

View File

@@ -9,14 +9,19 @@ export const uploadPopulationPromiseHOC = (
props?: UploadFeatureProps, props?: UploadFeatureProps,
): PopulationPromise<SerializedUploadNode> => { ): PopulationPromise<SerializedUploadNode> => {
const uploadPopulationPromise: PopulationPromise<SerializedUploadNode> = ({ const uploadPopulationPromise: PopulationPromise<SerializedUploadNode> = ({
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises,
field, field,
findMany,
flattenLocales,
node, node,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
@@ -41,10 +46,14 @@ export const uploadPopulationPromiseHOC = (
} }
if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) { if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
recurseNestedFields({ recurseNestedFields({
context,
currentDepth, currentDepth,
data: node.fields || {}, data: node.fields || {},
depth, depth,
editorPopulationPromises,
fields: props?.collections?.[node?.relationTo]?.fields, fields: props?.collections?.[node?.relationTo]?.fields,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises, promises,

View File

@@ -2,6 +2,7 @@ import type { Transformer } from '@lexical/markdown'
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical' import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
import type { SerializedLexicalNode } from 'lexical' import type { SerializedLexicalNode } from 'lexical'
import type { LexicalNodeReplacement } from 'lexical' import type { LexicalNodeReplacement } from 'lexical'
import type { RequestContext } from 'payload'
import type { SanitizedConfig } from 'payload/config' import type { SanitizedConfig } from 'payload/config'
import type { PayloadRequest, RichTextField, ValidateOptions } from 'payload/types' import type { PayloadRequest, RichTextField, ValidateOptions } from 'payload/types'
import type React from 'react' import type React from 'react'
@@ -13,9 +14,13 @@ import type { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeahe
import type { HTMLConverter } from './converters/html/converter/types' import type { HTMLConverter } from './converters/html/converter/types'
export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises,
field, field,
findMany,
flattenLocales,
node, node,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
@@ -23,12 +28,19 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}: { }: {
context: RequestContext
currentDepth: number currentDepth: number
depth: number depth: number
/**
* This maps all population promises to the node type
*/
editorPopulationPromises: Map<string, Array<PopulationPromise>>
field: RichTextField<SerializedEditorState, AdapterProps> field: RichTextField<SerializedEditorState, AdapterProps>
findMany: boolean
flattenLocales: boolean
node: T node: T
overrideAccess: boolean overrideAccess: boolean
populationPromises: Map<string, Array<PopulationPromise>> populationPromises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>

View File

@@ -148,10 +148,14 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
} }
}, },
populationPromise({ populationPromise({
context,
currentDepth, currentDepth,
depth, depth,
field, field,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
@@ -159,11 +163,15 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
// check if there are any features with nodes which have populationPromises for this field // check if there are any features with nodes which have populationPromises for this field
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) { if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
return richTextRelationshipPromise({ return richTextRelationshipPromise({
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
field, field,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises: finalSanitizedEditorConfig.features.populationPromises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,

View File

@@ -1,18 +1,24 @@
import type { Field, PayloadRequest, RichTextAdapter } from 'payload/types' import type { RequestContext } from 'payload'
import type { Field, PayloadRequest } from 'payload/types'
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload/types' import { afterReadTraverseFields } from 'payload/utilities'
import type { PopulationPromise } from '../field/features/types' import type { PopulationPromise } from '../field/features/types'
import { populate } from './populate'
type NestedRichTextFieldsArgs = { type NestedRichTextFieldsArgs = {
context: RequestContext
currentDepth?: number currentDepth?: number
data: unknown data: unknown
depth: number depth: number
/**
* This maps all the population promises to the node types
*/
editorPopulationPromises: Map<string, Array<PopulationPromise>>
fields: Field[] fields: Field[]
findMany: boolean
flattenLocales: boolean
overrideAccess: boolean overrideAccess: boolean
populationPromises: Map<string, Array<PopulationPromise>> populationPromises: Promise<void>[]
promises: Promise<void>[] promises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
@@ -20,10 +26,13 @@ type NestedRichTextFieldsArgs = {
} }
export const recurseNestedFields = ({ export const recurseNestedFields = ({
context,
currentDepth = 0, currentDepth = 0,
data, data,
depth, depth,
fields, fields,
findMany,
flattenLocales,
overrideAccess = false, overrideAccess = false,
populationPromises, populationPromises,
promises, promises,
@@ -31,193 +40,23 @@ export const recurseNestedFields = ({
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}: NestedRichTextFieldsArgs): void => { }: NestedRichTextFieldsArgs): void => {
fields.forEach((field) => { afterReadTraverseFields({
if (field.type === 'relationship' || field.type === 'upload') { collection: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
if (field.type === 'relationship') { context,
if (field.hasMany && Array.isArray(data[field.name])) { currentDepth,
if (Array.isArray(field.relationTo)) { depth,
// polymorphic relationship doc: data as any, // Looks like it's only needed for hooks and access control, so doesn't matter what we pass here right now
data[field.name].forEach(({ relationTo, value }, i) => { fieldPromises: promises, // Not sure if what I pass in here makes sense. But it doesn't seem like it's used at all anyways
const collection = req.payload.collections[relationTo] fields,
if (collection) { findMany,
promises.push( flattenLocales,
populate({ global: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
id: value, overrideAccess,
collection, populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end.
currentDepth, req,
data: data[field.name], showHiddenFields,
depth, siblingDoc,
field, triggerAccessControl: false, // TODO: Enable this to support access control
key: i, triggerHooks: false, // TODO: Enable this to support hooks
overrideAccess,
req,
showHiddenFields,
}),
)
}
})
} else {
data[field.name].forEach((id, i) => {
const collection = req.payload.collections[field.relationTo as string]
if (collection) {
promises.push(
populate({
id,
collection,
currentDepth,
data: data[field.name],
depth,
field,
key: i,
overrideAccess,
req,
showHiddenFields,
}),
)
}
})
}
} else if (
Array.isArray(field.relationTo) &&
data[field.name]?.value &&
data[field.name]?.relationTo
) {
const collection = req.payload.collections[data[field.name].relationTo]
promises.push(
populate({
id: data[field.name].value,
collection,
currentDepth,
data: data[field.name],
depth,
field,
key: 'value',
overrideAccess,
req,
showHiddenFields,
}),
)
}
}
if (typeof data[field.name] !== 'undefined' && typeof field.relationTo === 'string') {
if (!('hasMany' in field) || !field.hasMany) {
const collection = req.payload.collections[field.relationTo]
promises.push(
populate({
id: data[field.name],
collection,
currentDepth,
data,
depth,
field,
key: field.name,
overrideAccess,
req,
showHiddenFields,
}),
)
}
}
} else if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
if (fieldAffectsData(field) && typeof data[field.name] === 'object') {
recurseNestedFields({
currentDepth,
data: data[field.name],
depth,
fields: field.fields,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc,
})
} else {
recurseNestedFields({
currentDepth,
data,
depth,
fields: field.fields,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc,
})
}
} else if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
recurseNestedFields({
currentDepth,
data,
depth,
fields: tab.fields,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc,
})
})
} else if (Array.isArray(data[field.name])) {
if (field.type === 'blocks') {
data[field.name].forEach((row, i) => {
const block = field.blocks.find(({ slug }) => slug === row?.blockType)
if (block) {
recurseNestedFields({
currentDepth,
data: data[field.name][i],
depth,
fields: block.fields,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc: data[field.name][i], // This has to be scoped to the blocks's fields, otherwise there may be population issues, e.g. for a relationship field with Blocks Node, with a Blocks Field, with a RichText Field, With Relationship Node. The last richtext field would try to find itself using siblingDoc[field.nane], which only works if the siblingDoc is scoped to the blocks's fields
})
}
})
}
if (field.type === 'array') {
data[field.name].forEach((_, i) => {
recurseNestedFields({
currentDepth,
data: data[field.name][i],
depth,
fields: field.fields,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc, // TODO: if there's any population issues, this might have to be data[field.name][i] as well
})
})
}
}
if (field.type === 'richText') {
const editor: RichTextAdapter = field?.editor
if (editor?.populationPromise) {
const afterReadPromise = editor.populationPromise({
currentDepth,
depth,
field,
overrideAccess,
req,
showHiddenFields,
siblingDoc,
})
if (afterReadPromise) {
promises.push(afterReadPromise)
}
}
}
}) })
} }

View File

@@ -7,16 +7,16 @@ import type { AdapterProps } from '../types'
export type Args = Parameters< export type Args = Parameters<
RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromise'] RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromise']
>[0] & { >[0] & {
populationPromises: Map<string, Array<PopulationPromise>> editorPopulationPromises: Map<string, Array<PopulationPromise>>
} }
type RecurseRichTextArgs = { type RecurseRichTextArgs = {
children: SerializedLexicalNode[] children: SerializedLexicalNode[]
currentDepth: number currentDepth: number
depth: number depth: number
editorPopulationPromises: Map<string, Array<PopulationPromise>>
field: RichTextField<SerializedEditorState, AdapterProps> field: RichTextField<SerializedEditorState, AdapterProps>
overrideAccess: boolean overrideAccess: boolean
populationPromises: Map<string, Array<PopulationPromise>>
promises: Promise<void>[] promises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
@@ -25,29 +25,37 @@ type RecurseRichTextArgs = {
export const recurseRichText = ({ export const recurseRichText = ({
children, children,
context,
currentDepth = 0, currentDepth = 0,
depth, depth,
editorPopulationPromises,
field, field,
findMany,
flattenLocales,
overrideAccess = false, overrideAccess = false,
populationPromises, populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}: RecurseRichTextArgs): void => { }: RecurseRichTextArgs & Args): void => {
if (depth <= 0 || currentDepth > depth) { if (depth <= 0 || currentDepth > depth) {
return return
} }
if (Array.isArray(children)) { if (Array.isArray(children)) {
children.forEach((node) => { children.forEach((node) => {
if (populationPromises?.has(node.type)) { if (editorPopulationPromises?.has(node.type)) {
for (const promise of populationPromises.get(node.type)) { for (const promise of editorPopulationPromises.get(node.type)) {
promises.push( promises.push(
...promise({ ...promise({
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises,
field, field,
findMany,
flattenLocales,
node: node, node: node,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
@@ -62,9 +70,13 @@ export const recurseRichText = ({
if ('children' in node && Array.isArray(node?.children) && node?.children?.length) { if ('children' in node && Array.isArray(node?.children) && node?.children?.length) {
recurseRichText({ recurseRichText({
children: node.children as SerializedLexicalNode[], children: node.children as SerializedLexicalNode[],
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises,
field, field,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises, promises,
@@ -78,9 +90,13 @@ export const recurseRichText = ({
} }
export const richTextRelationshipPromise = async ({ export const richTextRelationshipPromise = async ({
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises,
field, field,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
req, req,
@@ -91,9 +107,13 @@ export const richTextRelationshipPromise = async ({
recurseRichText({ recurseRichText({
children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [], children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [],
context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises,
field, field,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises, promises,

View File

@@ -30,10 +30,14 @@ export function slateEditor(
} }
}, },
populationPromise({ populationPromise({
context,
currentDepth, currentDepth,
depth, depth,
field, field,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
@@ -45,10 +49,14 @@ export function slateEditor(
!field?.admin?.elements !field?.admin?.elements
) { ) {
return richTextRelationshipPromise({ return richTextRelationshipPromise({
context,
currentDepth, currentDepth,
depth, depth,
field, field,
findMany,
flattenLocales,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,

View File

@@ -1,6 +1,7 @@
import type { Block } from '../../../../packages/payload/src/fields/config/types' import type { Block } from '../../../../packages/payload/src/fields/config/types'
import { lexicalEditor } from '../../../../packages/richtext-lexical/src' import { lexicalEditor } from '../../../../packages/richtext-lexical/src'
import { textFieldsSlug } from '../Text/shared'
export const BlockColumns: any = { export const BlockColumns: any = {
type: 'array', type: 'array',
@@ -123,6 +124,18 @@ export const UploadAndRichTextBlock: Block = {
slug: 'uploadAndRichText', slug: 'uploadAndRichText',
} }
export const RelationshipHasManyBlock: Block = {
fields: [
{
name: 'rel',
type: 'relationship',
hasMany: true,
relationTo: [textFieldsSlug, 'uploads'],
required: true,
},
],
slug: 'relationshipHasManyBlock',
}
export const RelationshipBlock: Block = { export const RelationshipBlock: Block = {
fields: [ fields: [
{ {

View File

@@ -84,6 +84,26 @@ export function generateLexicalRichText() {
blockType: 'relationshipBlock', blockType: 'relationshipBlock',
}, },
}, },
{
format: '',
type: 'block',
version: 2,
fields: {
id: '6565c8668294bf824c24d4a4',
blockName: '',
blockType: 'relationshipHasManyBlock',
rel: [
{
value: '{{TEXT_DOC_ID}}',
relationTo: 'text-fields',
},
{
value: '{{UPLOAD_DOC_ID}}',
relationTo: 'uploads',
},
],
},
},
{ {
format: '', format: '',
type: 'block', type: 'block',

View File

@@ -12,6 +12,7 @@ import {
ConditionalLayoutBlock, ConditionalLayoutBlock,
RadioButtonsBlock, RadioButtonsBlock,
RelationshipBlock, RelationshipBlock,
RelationshipHasManyBlock,
RichTextBlock, RichTextBlock,
SelectFieldBlock, SelectFieldBlock,
SubBlockBlock, SubBlockBlock,
@@ -49,6 +50,7 @@ export const LexicalFields: CollectionConfig = {
UploadAndRichTextBlock, UploadAndRichTextBlock,
SelectFieldBlock, SelectFieldBlock,
RelationshipBlock, RelationshipBlock,
RelationshipHasManyBlock,
SubBlockBlock, SubBlockBlock,
RadioButtonsBlock, RadioButtonsBlock,
ConditionalLayoutBlock, ConditionalLayoutBlock,
@@ -102,6 +104,7 @@ export const LexicalFields: CollectionConfig = {
UploadAndRichTextBlock, UploadAndRichTextBlock,
SelectFieldBlock, SelectFieldBlock,
RelationshipBlock, RelationshipBlock,
RelationshipHasManyBlock,
SubBlockBlock, SubBlockBlock,
RadioButtonsBlock, RadioButtonsBlock,
ConditionalLayoutBlock, ConditionalLayoutBlock,

View File

@@ -191,7 +191,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded() await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible() await expect(richTextField).toBeVisible()
const lexicalBlock = richTextField.locator('.lexical-block').nth(1) // second: "Block Node, with RichText Field, with Relationship Node" const lexicalBlock = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with RichText Field, with Relationship Node"
await lexicalBlock.scrollIntoViewIfNeeded() await lexicalBlock.scrollIntoViewIfNeeded()
await expect(lexicalBlock).toBeVisible() await expect(lexicalBlock).toBeVisible()
@@ -225,7 +225,7 @@ describe('lexical', () => {
).docs[0] as never ).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[3] as SerializedBlockNode const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
const textNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1].children[0] const textNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1].children[0]
expect(textNodeInBlockNodeRichText.text).toBe( expect(textNodeInBlockNodeRichText.text).toBe(
@@ -239,7 +239,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded() await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible() await expect(richTextField).toBeVisible()
const lexicalBlock = richTextField.locator('.lexical-block').nth(1) // second: "Block Node, with RichText Field, with Relationship Node" const lexicalBlock = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with RichText Field, with Relationship Node"
await lexicalBlock.scrollIntoViewIfNeeded() await lexicalBlock.scrollIntoViewIfNeeded()
await expect(lexicalBlock).toBeVisible() await expect(lexicalBlock).toBeVisible()
@@ -298,7 +298,7 @@ describe('lexical', () => {
).docs[0] as never ).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[3] as SerializedBlockNode const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
const paragraphNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1] const paragraphNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1]
expect(paragraphNodeInBlockNodeRichText.children).toHaveLength(2) expect(paragraphNodeInBlockNodeRichText.children).toHaveLength(2)
@@ -319,7 +319,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded() await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible() await expect(richTextField).toBeVisible()
const lexicalBlock = richTextField.locator('.lexical-block').nth(1) // secondL: "Block Node, with RichText Field, with Relationship Node" const lexicalBlock = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with RichText Field, with Relationship Node"
await lexicalBlock.scrollIntoViewIfNeeded() await lexicalBlock.scrollIntoViewIfNeeded()
await expect(lexicalBlock).toBeVisible() await expect(lexicalBlock).toBeVisible()
@@ -388,7 +388,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded() await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible() await expect(richTextField).toBeVisible()
const lexicalBlock = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with Blocks Field, With RichText Field, With Relationship Node" const lexicalBlock = richTextField.locator('.lexical-block').nth(3) // third: "Block Node, with Blocks Field, With RichText Field, With Relationship Node"
await lexicalBlock.scrollIntoViewIfNeeded() await lexicalBlock.scrollIntoViewIfNeeded()
await expect(lexicalBlock).toBeVisible() await expect(lexicalBlock).toBeVisible()
@@ -441,7 +441,7 @@ describe('lexical', () => {
).docs[0] as never ).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode const blockNode: SerializedBlockNode = lexicalField.root.children[5] as SerializedBlockNode
const subBlocks = blockNode.fields.subBlocks const subBlocks = blockNode.fields.subBlocks
expect(subBlocks).toHaveLength(2) expect(subBlocks).toHaveLength(2)
@@ -459,9 +459,9 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded() await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible() await expect(richTextField).toBeVisible()
const radioButtonBlock1 = richTextField.locator('.lexical-block').nth(4) const radioButtonBlock1 = richTextField.locator('.lexical-block').nth(5)
const radioButtonBlock2 = richTextField.locator('.lexical-block').nth(5) const radioButtonBlock2 = richTextField.locator('.lexical-block').nth(6)
await radioButtonBlock2.scrollIntoViewIfNeeded() await radioButtonBlock2.scrollIntoViewIfNeeded()
await expect(radioButtonBlock1).toBeVisible() await expect(radioButtonBlock1).toBeVisible()
await expect(radioButtonBlock2).toBeVisible() await expect(radioButtonBlock2).toBeVisible()
@@ -507,8 +507,8 @@ describe('lexical', () => {
).docs[0] as never ).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const radio1: SerializedBlockNode = lexicalField.root.children[7] as SerializedBlockNode const radio1: SerializedBlockNode = lexicalField.root.children[8] as SerializedBlockNode
const radio2: SerializedBlockNode = lexicalField.root.children[8] as SerializedBlockNode const radio2: SerializedBlockNode = lexicalField.root.children[9] as SerializedBlockNode
expect(radio1.fields.radioButtons).toBe('option2') expect(radio1.fields.radioButtons).toBe('option2')
expect(radio2.fields.radioButtons).toBe('option3') expect(radio2.fields.radioButtons).toBe('option3')
@@ -534,7 +534,7 @@ describe('lexical', () => {
await parentEditorParagraph.click() // Click works better than focus await parentEditorParagraph.click() // Click works better than focus
const blockWithRichTextEditor = richTextField.locator('.lexical-block').nth(1) // third: "Block Node, with Blocks Field, With RichText Field, With Relationship Node" const blockWithRichTextEditor = richTextField.locator('.lexical-block').nth(2) // third: "Block Node, with Blocks Field, With RichText Field, With Relationship Node"
await blockWithRichTextEditor.scrollIntoViewIfNeeded() await blockWithRichTextEditor.scrollIntoViewIfNeeded()
await expect(blockWithRichTextEditor).toBeVisible() await expect(blockWithRichTextEditor).toBeVisible()
@@ -567,7 +567,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded() await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible() await expect(richTextField).toBeVisible()
const conditionalArrayBlock = richTextField.locator('.lexical-block').nth(6) const conditionalArrayBlock = richTextField.locator('.lexical-block').nth(7)
await conditionalArrayBlock.scrollIntoViewIfNeeded() await conditionalArrayBlock.scrollIntoViewIfNeeded()
await expect(conditionalArrayBlock).toBeVisible() await expect(conditionalArrayBlock).toBeVisible()

View File

@@ -22,6 +22,7 @@ import { lexicalMigrateDocData } from './collections/LexicalMigrate/data'
import { richTextDocData } from './collections/RichText/data' import { richTextDocData } from './collections/RichText/data'
import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText' import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText'
import { textDoc } from './collections/Text/shared' import { textDoc } from './collections/Text/shared'
import { uploadsDoc } from './collections/Upload/shared'
import { clearAndSeedEverything } from './seed' import { clearAndSeedEverything } from './seed'
import { import {
arrayFieldsSlug, arrayFieldsSlug,
@@ -331,6 +332,73 @@ describe('Lexical', () => {
expect(relationshipBlockNode.fields.rel.filename).toStrictEqual('payload.jpg') expect(relationshipBlockNode.fields.rel.filename).toStrictEqual('payload.jpg')
}) })
it('should correctly populate polymorphic hasMany relationships in blocks with depth=0', async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
where: {
title: {
equals: lexicalDocData.title,
},
},
depth: 0,
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks
const relationshipBlockNode: SerializedBlockNode = lexicalField.root
.children[3] as SerializedBlockNode
/**
* Depth 0 population:
*/
expect(Object.keys(relationshipBlockNode.fields.rel[0])).toHaveLength(2)
expect(relationshipBlockNode.fields.rel[0].relationTo).toStrictEqual('text-fields')
expect(relationshipBlockNode.fields.rel[0].value).toStrictEqual(createdTextDocID)
expect(Object.keys(relationshipBlockNode.fields.rel[1])).toHaveLength(2)
expect(relationshipBlockNode.fields.rel[1].relationTo).toStrictEqual('uploads')
expect(relationshipBlockNode.fields.rel[1].value).toStrictEqual(createdJPGDocID)
})
it('should correctly populate polymorphic hasMany relationships in blocks with depth=1', async () => {
// Related issue: https://github.com/payloadcms/payload/issues/4277
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
where: {
title: {
equals: lexicalDocData.title,
},
},
depth: 1,
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks
const relationshipBlockNode: SerializedBlockNode = lexicalField.root
.children[3] as SerializedBlockNode
/**
* Depth 1 population:
*/
expect(Object.keys(relationshipBlockNode.fields.rel[0])).toHaveLength(2)
expect(relationshipBlockNode.fields.rel[0].relationTo).toStrictEqual('text-fields')
expect(relationshipBlockNode.fields.rel[0].value.id).toStrictEqual(createdTextDocID)
expect(relationshipBlockNode.fields.rel[0].value.text).toStrictEqual(textDoc.text)
expect(relationshipBlockNode.fields.rel[0].value.localizedText).toStrictEqual(
textDoc.localizedText,
)
expect(Object.keys(relationshipBlockNode.fields.rel[1])).toHaveLength(2)
expect(relationshipBlockNode.fields.rel[1].relationTo).toStrictEqual('uploads')
expect(relationshipBlockNode.fields.rel[1].value.id).toStrictEqual(createdJPGDocID)
expect(relationshipBlockNode.fields.rel[1].value.text).toStrictEqual(uploadsDoc.text)
expect(relationshipBlockNode.fields.rel[1].value.filename).toStrictEqual('payload.jpg')
})
it('should not populate relationship nodes inside of a sub-editor from a blocks node with 0 depth', async () => { it('should not populate relationship nodes inside of a sub-editor from a blocks node with 0 depth', async () => {
const lexicalDoc: LexicalField = ( const lexicalDoc: LexicalField = (
await payload.find({ await payload.find({
@@ -347,7 +415,7 @@ describe('Lexical', () => {
const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks
const subEditorBlockNode: SerializedBlockNode = lexicalField.root const subEditorBlockNode: SerializedBlockNode = lexicalField.root
.children[3] as SerializedBlockNode .children[4] as SerializedBlockNode
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText
@@ -378,7 +446,7 @@ describe('Lexical', () => {
const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks
const subEditorBlockNode: SerializedBlockNode = lexicalField.root const subEditorBlockNode: SerializedBlockNode = lexicalField.root
.children[3] as SerializedBlockNode .children[4] as SerializedBlockNode
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText
@@ -425,7 +493,7 @@ describe('Lexical', () => {
const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks
const subEditorBlockNode: SerializedBlockNode = lexicalField.root const subEditorBlockNode: SerializedBlockNode = lexicalField.root
.children[3] as SerializedBlockNode .children[4] as SerializedBlockNode
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText