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

View File

@@ -1,21 +1,24 @@
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 { combineMerge } from '../utilities/combineMerge'
export {
configToJSONSchema,
entityToJSONSchema,
withNullableJSONSchemaType,
} from '../utilities/configToJSONSchema'
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
export { deepCopyObject } from '../utilities/deepCopyObject'
export { deepCopyObject } from '../utilities/deepCopyObject'
export { deepMerge } from '../utilities/deepMerge'
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON'
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields'
export { formatLabels, formatNames, toWords } from '../utilities/formatLabels'
export { getIDType } from '../utilities/getIDType'
export { getTranslation } from '../utilities/getTranslation'
export { isValidID } from '../utilities/isValidID'

View File

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

View File

@@ -21,6 +21,8 @@ type Args = {
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
triggerAccessControl?: boolean
triggerHooks?: boolean
}
export const traverseFields = ({
@@ -39,6 +41,8 @@ export const traverseFields = ({
req,
showHiddenFields,
siblingDoc,
triggerAccessControl = true,
triggerHooks = true,
}: Args): void => {
fields.forEach((field) => {
fieldPromises.push(
@@ -58,6 +62,8 @@ export const traverseFields = ({
req,
showHiddenFields,
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.
if (editor?.populationPromise) {
await editor?.populationPromise({
context,
depth,
field,
findMany: false,
flattenLocales: false,
overrideAccess: false,
populationPromises: [],
req: context.req,
showHiddenFields: false,
siblingDoc: parent,

View File

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

View File

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

View File

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

View File

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

View File

@@ -148,10 +148,14 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}
},
populationPromise({
context,
currentDepth,
depth,
field,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
showHiddenFields,
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
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
return richTextRelationshipPromise({
context,
currentDepth,
depth,
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
field,
findMany,
flattenLocales,
overrideAccess,
populationPromises: finalSanitizedEditorConfig.features.populationPromises,
populationPromises,
req,
showHiddenFields,
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 { populate } from './populate'
type NestedRichTextFieldsArgs = {
context: RequestContext
currentDepth?: number
data: unknown
depth: number
/**
* This maps all the population promises to the node types
*/
editorPopulationPromises: Map<string, Array<PopulationPromise>>
fields: Field[]
findMany: boolean
flattenLocales: boolean
overrideAccess: boolean
populationPromises: Map<string, Array<PopulationPromise>>
populationPromises: Promise<void>[]
promises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
@@ -20,10 +26,13 @@ type NestedRichTextFieldsArgs = {
}
export const recurseNestedFields = ({
context,
currentDepth = 0,
data,
depth,
fields,
findMany,
flattenLocales,
overrideAccess = false,
populationPromises,
promises,
@@ -31,193 +40,23 @@ export const recurseNestedFields = ({
showHiddenFields,
siblingDoc,
}: NestedRichTextFieldsArgs): void => {
fields.forEach((field) => {
if (field.type === 'relationship' || field.type === 'upload') {
if (field.type === 'relationship') {
if (field.hasMany && Array.isArray(data[field.name])) {
if (Array.isArray(field.relationTo)) {
// polymorphic relationship
data[field.name].forEach(({ relationTo, value }, i) => {
const collection = req.payload.collections[relationTo]
if (collection) {
promises.push(
populate({
id: value,
collection,
afterReadTraverseFields({
collection: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
context,
currentDepth,
data: data[field.name],
depth,
field,
key: i,
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
fieldPromises: promises, // Not sure if what I pass in here makes sense. But it doesn't seem like it's used at all anyways
fields,
findMany,
flattenLocales,
global: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
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,
populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end.
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)
}
}
}
triggerAccessControl: false, // TODO: Enable this to support access control
triggerHooks: false, // TODO: Enable this to support hooks
})
}

View File

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

View File

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

View File

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

View File

@@ -84,6 +84,26 @@ export function generateLexicalRichText() {
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: '',
type: 'block',

View File

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

View File

@@ -191,7 +191,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded()
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 expect(lexicalBlock).toBeVisible()
@@ -225,7 +225,7 @@ describe('lexical', () => {
).docs[0] as never
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]
expect(textNodeInBlockNodeRichText.text).toBe(
@@ -239,7 +239,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded()
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 expect(lexicalBlock).toBeVisible()
@@ -298,7 +298,7 @@ describe('lexical', () => {
).docs[0] as never
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]
expect(paragraphNodeInBlockNodeRichText.children).toHaveLength(2)
@@ -319,7 +319,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded()
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 expect(lexicalBlock).toBeVisible()
@@ -388,7 +388,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded()
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 expect(lexicalBlock).toBeVisible()
@@ -441,7 +441,7 @@ describe('lexical', () => {
).docs[0] as never
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
expect(subBlocks).toHaveLength(2)
@@ -459,9 +459,9 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded()
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 expect(radioButtonBlock1).toBeVisible()
await expect(radioButtonBlock2).toBeVisible()
@@ -507,8 +507,8 @@ describe('lexical', () => {
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const radio1: SerializedBlockNode = lexicalField.root.children[7] as SerializedBlockNode
const radio2: SerializedBlockNode = lexicalField.root.children[8] as SerializedBlockNode
const radio1: SerializedBlockNode = lexicalField.root.children[8] as SerializedBlockNode
const radio2: SerializedBlockNode = lexicalField.root.children[9] as SerializedBlockNode
expect(radio1.fields.radioButtons).toBe('option2')
expect(radio2.fields.radioButtons).toBe('option3')
@@ -534,7 +534,7 @@ describe('lexical', () => {
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 expect(blockWithRichTextEditor).toBeVisible()
@@ -567,7 +567,7 @@ describe('lexical', () => {
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const conditionalArrayBlock = richTextField.locator('.lexical-block').nth(6)
const conditionalArrayBlock = richTextField.locator('.lexical-block').nth(7)
await conditionalArrayBlock.scrollIntoViewIfNeeded()
await expect(conditionalArrayBlock).toBeVisible()

View File

@@ -22,6 +22,7 @@ import { lexicalMigrateDocData } from './collections/LexicalMigrate/data'
import { richTextDocData } from './collections/RichText/data'
import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText'
import { textDoc } from './collections/Text/shared'
import { uploadsDoc } from './collections/Upload/shared'
import { clearAndSeedEverything } from './seed'
import {
arrayFieldsSlug,
@@ -331,6 +332,73 @@ describe('Lexical', () => {
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 () => {
const lexicalDoc: LexicalField = (
await payload.find({
@@ -347,7 +415,7 @@ describe('Lexical', () => {
const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks
const subEditorBlockNode: SerializedBlockNode = lexicalField.root
.children[3] as SerializedBlockNode
.children[4] as SerializedBlockNode
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText
@@ -378,7 +446,7 @@ describe('Lexical', () => {
const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks
const subEditorBlockNode: SerializedBlockNode = lexicalField.root
.children[3] as SerializedBlockNode
.children[4] as SerializedBlockNode
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText
@@ -425,7 +493,7 @@ describe('Lexical', () => {
const lexicalField: SerializedEditorState = lexicalDoc?.lexicalWithBlocks
const subEditorBlockNode: SerializedBlockNode = lexicalField.root
.children[3] as SerializedBlockNode
.children[4] as SerializedBlockNode
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText