From 094d02ce1d85106470a1a8c6ffe9050873f2e57a Mon Sep 17 00:00:00 2001 From: Alessio Gravili <70709113+AlessioGr@users.noreply.github.com> Date: Tue, 28 Nov 2023 19:18:07 +0100 Subject: [PATCH] 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 --- .../forms/field-types/RichText/types.ts | 5 + packages/payload/src/exports/utilities.ts | 9 +- .../src/fields/hooks/afterRead/promise.ts | 32 ++- .../fields/hooks/afterRead/traverseFields.ts | 6 + .../src/graphql/schema/buildObjectType.ts | 5 + .../features/Blocks/populationPromise.ts | 9 + .../field/features/Link/populationPromise.ts | 9 + .../features/Upload/populationPromise.ts | 9 + .../src/field/features/types.ts | 14 +- packages/richtext-lexical/src/index.ts | 10 +- .../src/populate/recurseNestedFields.ts | 225 +++--------------- .../populate/richTextRelationshipPromise.ts | 30 ++- packages/richtext-slate/src/index.ts | 8 + test/fields/collections/Lexical/blocks.ts | 13 + .../Lexical/generateLexicalRichText.ts | 20 ++ test/fields/collections/Lexical/index.ts | 3 + test/fields/lexical.e2e.spec.ts | 26 +- test/fields/lexical.int.spec.ts | 74 +++++- 18 files changed, 284 insertions(+), 223 deletions(-) diff --git a/packages/payload/src/admin/components/forms/field-types/RichText/types.ts b/packages/payload/src/admin/components/forms/field-types/RichText/types.ts index b578f302f..c031739aa 100644 --- a/packages/payload/src/admin/components/forms/field-types/RichText/types.ts +++ b/packages/payload/src/admin/components/forms/field-types/RichText/types.ts @@ -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 + findMany: boolean + flattenLocales: boolean overrideAccess?: boolean + populationPromises: Promise[] req: PayloadRequest showHiddenFields: boolean siblingDoc: Record diff --git a/packages/payload/src/exports/utilities.ts b/packages/payload/src/exports/utilities.ts index f102b9f4f..ba700a776 100644 --- a/packages/payload/src/exports/utilities.ts +++ b/packages/payload/src/exports/utilities.ts @@ -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' diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index c464c09cb..1a2d9d33f 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -25,12 +25,14 @@ type Args = { req: PayloadRequest showHiddenFields: boolean siblingDoc: Record + 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 => { 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 } diff --git a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts index 638ebcf69..d3c68cf8c 100644 --- a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts +++ b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts @@ -21,6 +21,8 @@ type Args = { req: PayloadRequest showHiddenFields: boolean siblingDoc: Record + 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, }), ) }) diff --git a/packages/payload/src/graphql/schema/buildObjectType.ts b/packages/payload/src/graphql/schema/buildObjectType.ts index a901afcdd..e75d18a56 100644 --- a/packages/payload/src/graphql/schema/buildObjectType.ts +++ b/packages/payload/src/graphql/schema/buildObjectType.ts @@ -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, diff --git a/packages/richtext-lexical/src/field/features/Blocks/populationPromise.ts b/packages/richtext-lexical/src/field/features/Blocks/populationPromise.ts index 710be6608..85215d6bb 100644 --- a/packages/richtext-lexical/src/field/features/Blocks/populationPromise.ts +++ b/packages/richtext-lexical/src/field/features/Blocks/populationPromise.ts @@ -12,13 +12,18 @@ export const blockPopulationPromiseHOC = ( props: BlocksFeatureProps, ): PopulationPromise => { const blockPopulationPromise: PopulationPromise = ({ + 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, diff --git a/packages/richtext-lexical/src/field/features/Link/populationPromise.ts b/packages/richtext-lexical/src/field/features/Link/populationPromise.ts index 53ab4dade..537f90a3b 100644 --- a/packages/richtext-lexical/src/field/features/Link/populationPromise.ts +++ b/packages/richtext-lexical/src/field/features/Link/populationPromise.ts @@ -9,14 +9,19 @@ export const linkPopulationPromiseHOC = ( props: LinkFeatureProps, ): PopulationPromise => { const linkPopulationPromise: PopulationPromise = ({ + context, currentDepth, depth, + editorPopulationPromises, field, + findMany, + flattenLocales, node, overrideAccess, populationPromises, req, showHiddenFields, + siblingDoc, }) => { const promises: Promise[] = [] @@ -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, diff --git a/packages/richtext-lexical/src/field/features/Upload/populationPromise.ts b/packages/richtext-lexical/src/field/features/Upload/populationPromise.ts index 1bf823190..8fd9fd7b5 100644 --- a/packages/richtext-lexical/src/field/features/Upload/populationPromise.ts +++ b/packages/richtext-lexical/src/field/features/Upload/populationPromise.ts @@ -9,14 +9,19 @@ export const uploadPopulationPromiseHOC = ( props?: UploadFeatureProps, ): PopulationPromise => { const uploadPopulationPromise: PopulationPromise = ({ + context, currentDepth, depth, + editorPopulationPromises, field, + findMany, + flattenLocales, node, overrideAccess, populationPromises, req, showHiddenFields, + siblingDoc, }) => { const promises: Promise[] = [] @@ -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, diff --git a/packages/richtext-lexical/src/field/features/types.ts b/packages/richtext-lexical/src/field/features/types.ts index d1278b8b5..8ddc87fab 100644 --- a/packages/richtext-lexical/src/field/features/types.ts +++ b/packages/richtext-lexical/src/field/features/types.ts @@ -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 = ({ + context, currentDepth, depth, + editorPopulationPromises, field, + findMany, + flattenLocales, node, overrideAccess, populationPromises, @@ -23,12 +28,19 @@ export type PopulationPromise> field: RichTextField + findMany: boolean + flattenLocales: boolean node: T overrideAccess: boolean - populationPromises: Map> + populationPromises: Promise[] req: PayloadRequest showHiddenFields: boolean siblingDoc: Record diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index d559fbd95..5289c5c2a 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -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, diff --git a/packages/richtext-lexical/src/populate/recurseNestedFields.ts b/packages/richtext-lexical/src/populate/recurseNestedFields.ts index b6afc05ce..b5f080063 100644 --- a/packages/richtext-lexical/src/populate/recurseNestedFields.ts +++ b/packages/richtext-lexical/src/populate/recurseNestedFields.ts @@ -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> fields: Field[] + findMany: boolean + flattenLocales: boolean overrideAccess: boolean - populationPromises: Map> + populationPromises: Promise[] promises: Promise[] 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, - currentDepth, - data: data[field.name], - depth, - field, - key: i, - 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) - } - } - } + afterReadTraverseFields({ + collection: null, // Pass from core? This is only needed for hooks, so we can leave this null for now + context, + currentDepth, + depth, + 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, + populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end. + req, + showHiddenFields, + siblingDoc, + triggerAccessControl: false, // TODO: Enable this to support access control + triggerHooks: false, // TODO: Enable this to support hooks }) } diff --git a/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts b/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts index d5af36a61..82d81e659 100644 --- a/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts +++ b/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts @@ -7,16 +7,16 @@ import type { AdapterProps } from '../types' export type Args = Parameters< RichTextAdapter['populationPromise'] >[0] & { - populationPromises: Map> + editorPopulationPromises: Map> } type RecurseRichTextArgs = { children: SerializedLexicalNode[] currentDepth: number depth: number + editorPopulationPromises: Map> field: RichTextField overrideAccess: boolean - populationPromises: Map> promises: Promise[] 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, diff --git a/packages/richtext-slate/src/index.ts b/packages/richtext-slate/src/index.ts index 049952620..d20b799d3 100644 --- a/packages/richtext-slate/src/index.ts +++ b/packages/richtext-slate/src/index.ts @@ -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, diff --git a/test/fields/collections/Lexical/blocks.ts b/test/fields/collections/Lexical/blocks.ts index a5442dcf4..150006e61 100644 --- a/test/fields/collections/Lexical/blocks.ts +++ b/test/fields/collections/Lexical/blocks.ts @@ -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: [ { diff --git a/test/fields/collections/Lexical/generateLexicalRichText.ts b/test/fields/collections/Lexical/generateLexicalRichText.ts index 4dc645787..b38d2fd61 100644 --- a/test/fields/collections/Lexical/generateLexicalRichText.ts +++ b/test/fields/collections/Lexical/generateLexicalRichText.ts @@ -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', diff --git a/test/fields/collections/Lexical/index.ts b/test/fields/collections/Lexical/index.ts index 035be0fec..aea1f1b0e 100644 --- a/test/fields/collections/Lexical/index.ts +++ b/test/fields/collections/Lexical/index.ts @@ -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, diff --git a/test/fields/lexical.e2e.spec.ts b/test/fields/lexical.e2e.spec.ts index be35dc37b..353288a6b 100644 --- a/test/fields/lexical.e2e.spec.ts +++ b/test/fields/lexical.e2e.spec.ts @@ -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() diff --git a/test/fields/lexical.int.spec.ts b/test/fields/lexical.int.spec.ts index 4e9809be2..956404124 100644 --- a/test/fields/lexical.int.spec.ts +++ b/test/fields/lexical.int.spec.ts @@ -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