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:
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user