feat(richtext-lexical)!: rework how population works and saves data, improve node typing

This commit is contained in:
Alessio Gravili
2024-04-17 11:46:47 -04:00
parent 58ea94f6ac
commit 39ba39c237
52 changed files with 709 additions and 420 deletions

View File

@@ -470,19 +470,24 @@ function buildObjectType({
// is run here again, with the provided depth. // is run here again, with the provided depth.
// In the graphql find.ts resolver, the depth is then hard-coded to 0. // In the graphql find.ts resolver, the depth is then hard-coded to 0.
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise. // Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
if (editor?.populationPromise) { if (editor?.populationPromises) {
await editor?.populationPromise({ const fieldPromises = []
const populationPromises = []
editor?.populationPromises({
context, context,
depth, depth,
field, field,
fieldPromises,
findMany: false, findMany: false,
flattenLocales: false, flattenLocales: false,
overrideAccess: false, overrideAccess: false,
populationPromises: [], populationPromises,
req: context.req, req: context.req,
showHiddenFields: false, showHiddenFields: false,
siblingDoc: parent, siblingDoc: parent,
}) })
await Promise.all(fieldPromises)
await Promise.all(populationPromises)
} }
return parent[field.name] return parent[field.name]

View File

@@ -2,7 +2,7 @@ import type { I18n } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema' import type { JSONSchema4 } from 'json-schema'
import type { SanitizedConfig } from '../config/types.js' import type { SanitizedConfig } from '../config/types.js'
import type { Field, RichTextField, Validate } from '../fields/config/types.js' import type { Field, FieldBase, RichTextField, Validate } from '../fields/config/types.js'
import type { PayloadRequest, RequestContext } from '../types/index.js' import type { PayloadRequest, RequestContext } from '../types/index.js'
import type { WithServerSideProps } from './elements/WithServerSideProps.js' import type { WithServerSideProps } from './elements/WithServerSideProps.js'
@@ -19,15 +19,6 @@ type RichTextAdapterBase<
AdapterProps = any, AdapterProps = any,
ExtraFieldProperties = {}, ExtraFieldProperties = {},
> = { > = {
afterReadPromise?: ({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
incomingEditorState: Value
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
generateComponentMap: (args: { generateComponentMap: (args: {
WithServerSideProps: WithServerSideProps WithServerSideProps: WithServerSideProps
config: SanitizedConfig config: SanitizedConfig
@@ -40,6 +31,7 @@ type RichTextAdapterBase<
schemaMap: Map<string, Field[]> schemaMap: Map<string, Field[]>
schemaPath: string schemaPath: string
}) => Map<string, Field[]> }) => Map<string, Field[]>
hooks?: FieldBase['hooks']
outputSchema?: ({ outputSchema?: ({
collectionIDFieldTypes, collectionIDFieldTypes,
config, config,
@@ -56,11 +48,18 @@ type RichTextAdapterBase<
interfaceNameDefinitions: Map<string, JSONSchema4> interfaceNameDefinitions: Map<string, JSONSchema4>
isRequired: boolean isRequired: boolean
}) => JSONSchema4 }) => JSONSchema4
populationPromise?: (data: { /**
* Like an afterRead hook, but runs for both afterRead AND in the GraphQL resolver. For populating data, this should be used.
*
* To populate stuff / resolve field hooks, mutate the incoming populationPromises or fieldPromises array. They will then be awaited in the correct order within payload itself.
* @param data
*/
populationPromises?: (data: {
context: RequestContext context: RequestContext
currentDepth?: number currentDepth?: number
depth: number depth: number
field: RichTextField<Value, AdapterProps, ExtraFieldProperties> field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
fieldPromises: Promise<void>[]
findMany: boolean findMany: boolean
flattenLocales: boolean flattenLocales: boolean
overrideAccess?: boolean overrideAccess?: boolean
@@ -68,7 +67,7 @@ type RichTextAdapterBase<
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>
}) => Promise<void> | null }) => void
validate: Validate< validate: Validate<
Value, Value,
Value, Value,

View File

@@ -18,6 +18,9 @@ type Args = {
doc: Record<string, unknown> doc: Record<string, unknown>
fallbackLocale: null | string fallbackLocale: null | string
field: Field | TabAsField field: Field | TabAsField
/**
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
*/
fieldPromises: Promise<void>[] fieldPromises: Promise<void>[]
findMany: boolean findMany: boolean
flattenLocales: boolean flattenLocales: boolean
@@ -140,12 +143,13 @@ export const promise = async ({
case 'richText': { case 'richText': {
const editor: RichTextAdapter = field?.editor const editor: RichTextAdapter = field?.editor
// This is run here AND in the GraphQL Resolver // This is run here AND in the GraphQL Resolver
if (editor?.populationPromise) { if (editor?.populationPromises) {
const populationPromise = editor.populationPromise({ editor.populationPromises({
context, context,
currentDepth, currentDepth,
depth, depth,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
@@ -154,12 +158,8 @@ export const promise = async ({
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}) })
if (populationPromise) {
populationPromises.push(populationPromise)
}
} }
/*
// This is only run here, independent of depth // This is only run here, independent of depth
if (editor?.afterReadPromise) { if (editor?.afterReadPromise) {
const afterReadPromise = editor?.afterReadPromise({ const afterReadPromise = editor?.afterReadPromise({
@@ -171,7 +171,7 @@ export const promise = async ({
if (afterReadPromise) { if (afterReadPromise) {
populationPromises.push(afterReadPromise) populationPromises.push(afterReadPromise)
} }
} }*/ //TODO: HOOKS!
break break
} }

View File

@@ -12,6 +12,9 @@ type Args = {
depth: number depth: number
doc: Record<string, unknown> doc: Record<string, unknown>
fallbackLocale: null | string fallbackLocale: null | string
/**
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
*/
fieldPromises: Promise<void>[] fieldPromises: Promise<void>[]
fields: (Field | TabAsField)[] fields: (Field | TabAsField)[]
findMany: boolean findMany: boolean

View File

@@ -109,3 +109,7 @@ export type AllOperations = AuthOperations | Operation | VersionOperations
export function docHasTimestamps(doc: any): doc is TypeWithTimestamps { export function docHasTimestamps(doc: any): doc is TypeWithTimestamps {
return doc?.createdAt && doc?.updatedAt return doc?.createdAt && doc?.updatedAt
} }
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N // This is a commonly used trick to detect 'any'
export type IsAny<T> = IfAny<T, true, false>
export type ReplaceAny<T, DefaultType> = IsAny<T> extends true ? DefaultType : T

View File

@@ -1,10 +1,10 @@
import lexicalRichTextImport, { type SerializedQuoteNode } from '@lexical/rich-text' import lexicalRichTextImport from '@lexical/rich-text'
const { QuoteNode } = lexicalRichTextImport const { QuoteNode } = lexicalRichTextImport
import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js' import type { FeatureProviderProviderServer } from '../types.js'
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js' import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
import { createNode } from '../typeUtilities.js'
import { BlockQuoteFeatureClientComponent } from './feature.client.js' import { BlockQuoteFeatureClientComponent } from './feature.client.js'
import { MarkdownTransformer } from './markdownTransformer.js' import { MarkdownTransformer } from './markdownTransformer.js'
@@ -16,7 +16,7 @@ export const BlockQuoteFeature: FeatureProviderProviderServer<undefined, undefin
clientFeatureProps: null, clientFeatureProps: null,
markdownTransformers: [MarkdownTransformer], markdownTransformers: [MarkdownTransformer],
nodes: [ nodes: [
{ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, payload }) => { converter: async ({ converters, node, parent, payload }) => {
@@ -33,10 +33,10 @@ export const BlockQuoteFeature: FeatureProviderProviderServer<undefined, undefin
return `<blockquote>${childrenText}</blockquote>` return `<blockquote>${childrenText}</blockquote>`
}, },
nodeTypes: [QuoteNode.getType()], nodeTypes: [QuoteNode.getType()],
} as HTMLConverter<SerializedQuoteNode>, },
}, },
node: QuoteNode, node: QuoteNode,
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -125,6 +125,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
reducedBlock && reducedBlock &&
initialState !== false && ( initialState !== false && (
<Form <Form
beforeSubmit={[onChange]}
// @ts-expect-error TODO: Fix this // @ts-expect-error TODO: Fix this
fields={fieldMap} fields={fieldMap}
initialState={initialState} initialState={initialState}

View File

@@ -8,6 +8,7 @@ import type { FeatureProviderProviderServer } from '../types.js'
import type { BlocksFeatureClientProps } from './feature.client.js' import type { BlocksFeatureClientProps } from './feature.client.js'
import { cloneDeep } from '../../lexical/utils/cloneDeep.js' import { cloneDeep } from '../../lexical/utils/cloneDeep.js'
import { createNode } from '../typeUtilities.js'
import { BlocksFeatureClientComponent } from './feature.client.js' import { BlocksFeatureClientComponent } from './feature.client.js'
import { BlockNode } from './nodes/BlocksNode.js' import { BlockNode } from './nodes/BlocksNode.js'
import { blockPopulationPromiseHOC } from './populationPromise.js' import { blockPopulationPromiseHOC } from './populationPromise.js'
@@ -131,11 +132,11 @@ export const BlocksFeature: FeatureProviderProviderServer<
}, },
}, },
nodes: [ nodes: [
{ createNode({
node: BlockNode, node: BlockNode,
populationPromises: [blockPopulationPromiseHOC(props)], populationPromises: [blockPopulationPromiseHOC(props)],
validations: [blockValidationHOC(props)], validations: [blockValidationHOC(props)],
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -16,6 +16,7 @@ export const blockPopulationPromiseHOC = (
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises, editorPopulationPromises,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
node, node,
@@ -28,8 +29,6 @@ export const blockPopulationPromiseHOC = (
const blocks: Block[] = props.blocks const blocks: Block[] = props.blocks
const blockFieldData = node.fields const blockFieldData = node.fields
const promises: Promise<void>[] = []
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here // Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
const payloadConfig = req.payload.config const payloadConfig = req.payload.config
const validRelationships = payloadConfig.collections.map((c) => c.slug) || [] const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
@@ -45,7 +44,7 @@ export const blockPopulationPromiseHOC = (
// find block used in this node // find block used in this node
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType) const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
if (!block || !block?.fields?.length || !blockFieldData) { if (!block || !block?.fields?.length || !blockFieldData) {
return promises return
} }
recurseNestedFields({ recurseNestedFields({
@@ -54,19 +53,17 @@ export const blockPopulationPromiseHOC = (
data: blockFieldData, data: blockFieldData,
depth, depth,
editorPopulationPromises, editorPopulationPromises,
fieldPromises,
fields: block.fields, fields: block.fields,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises,
req, req,
showHiddenFields, showHiddenFields,
// The afterReadPromise gets its data from looking for field.name inside the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field. // The afterReadPromise gets its data from looking for field.name inside the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field.
siblingDoc: blockFieldData, siblingDoc: blockFieldData,
}) })
return promises
} }
return blockPopulationPromise return blockPopulationPromise

View File

@@ -7,6 +7,7 @@ import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js' import type { FeatureProviderProviderServer } from '../types.js'
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js' import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
import { createNode } from '../typeUtilities.js'
import { HeadingFeatureClientComponent } from './feature.client.js' import { HeadingFeatureClientComponent } from './feature.client.js'
import { MarkdownTransformer } from './markdownTransformer.js' import { MarkdownTransformer } from './markdownTransformer.js'
@@ -30,8 +31,7 @@ export const HeadingFeature: FeatureProviderProviderServer<
ClientComponent: HeadingFeatureClientComponent, ClientComponent: HeadingFeatureClientComponent,
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
nodes: [ nodes: [
{ createNode({
type: HeadingNode.getType(),
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, payload }) => { converter: async ({ converters, node, parent, payload }) => {
@@ -48,10 +48,10 @@ export const HeadingFeature: FeatureProviderProviderServer<
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>' return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
}, },
nodeTypes: [HeadingNode.getType()], nodeTypes: [HeadingNode.getType()],
} as HTMLConverter<SerializedHeadingNode>, },
}, },
node: HeadingNode, node: HeadingNode,
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -1,7 +1,6 @@
import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js' import type { FeatureProviderProviderServer } from '../types.js'
import type { SerializedHorizontalRuleNode } from './nodes/HorizontalRuleNode.js'
import { createNode } from '../typeUtilities.js'
import { HorizontalRuleFeatureClientComponent } from './feature.client.js' import { HorizontalRuleFeatureClientComponent } from './feature.client.js'
import { MarkdownTransformer } from './markdownTransformer.js' import { MarkdownTransformer } from './markdownTransformer.js'
import { HorizontalRuleNode } from './nodes/HorizontalRuleNode.js' import { HorizontalRuleNode } from './nodes/HorizontalRuleNode.js'
@@ -16,17 +15,17 @@ export const HorizontalRuleFeature: FeatureProviderProviderServer<undefined, und
clientFeatureProps: null, clientFeatureProps: null,
markdownTransformers: [MarkdownTransformer], markdownTransformers: [MarkdownTransformer],
nodes: [ nodes: [
{ createNode({
converters: { converters: {
html: { html: {
converter: () => { converter: () => {
return `<hr/>` return `<hr/>`
}, },
nodeTypes: [HorizontalRuleNode.getType()], nodeTypes: [HorizontalRuleNode.getType()],
} as HTMLConverter<SerializedHorizontalRuleNode>, },
}, },
node: HorizontalRuleNode, node: HorizontalRuleNode,
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -3,13 +3,13 @@ import type { SanitizedConfig } from 'payload/config'
import type { Field, FieldWithRichTextRequiredEditor } from 'payload/types' import type { Field, FieldWithRichTextRequiredEditor } from 'payload/types'
import { traverseFields } from '@payloadcms/next/utilities' import { traverseFields } from '@payloadcms/next/utilities'
import { deepCopyObject } from 'payload/utilities'
import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js' import type { FeatureProviderProviderServer } from '../types.js'
import type { ClientProps } from './feature.client.js' import type { ClientProps } from './feature.client.js'
import type { SerializedAutoLinkNode, SerializedLinkNode } from './nodes/types.js'
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js' import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
import { createNode } from '../typeUtilities.js'
import { LinkFeatureClientComponent } from './feature.client.js' import { LinkFeatureClientComponent } from './feature.client.js'
import { AutoLinkNode } from './nodes/AutoLinkNode.js' import { AutoLinkNode } from './nodes/AutoLinkNode.js'
import { LinkNode } from './nodes/LinkNode.js' import { LinkNode } from './nodes/LinkNode.js'
@@ -67,21 +67,26 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
enabledCollections: props.enabledCollections, enabledCollections: props.enabledCollections,
} as ExclusiveLinkCollectionsProps, } as ExclusiveLinkCollectionsProps,
generateSchemaMap: ({ config, i18n, props }) => { generateSchemaMap: ({ config, i18n, props }) => {
if (!props?.fields || !Array.isArray(props.fields) || props.fields.length === 0) {
return null
}
const schemaMap = new Map<string, Field[]>()
const validRelationships = config.collections.map((c) => c.slug) || []
const transformedFields = transformExtraFields( const transformedFields = transformExtraFields(
props.fields, deepCopyObject(props.fields),
config, config,
i18n, i18n,
props.enabledCollections, props.enabledCollections,
props.disabledCollections, props.disabledCollections,
) )
if (
!transformedFields ||
!Array.isArray(transformedFields) ||
transformedFields.length === 0
) {
return null
}
const schemaMap = new Map<string, Field[]>()
const validRelationships = config.collections.map((c) => c.slug) || []
schemaMap.set('fields', transformedFields) schemaMap.set('fields', transformedFields)
traverseFields({ traverseFields({
@@ -96,7 +101,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
return schemaMap return schemaMap
}, },
nodes: [ nodes: [
{ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, payload }) => { converter: async ({ converters, node, parent, payload }) => {
@@ -123,12 +128,19 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
return `<a href="${href}"${rel}>${childrenText}</a>` return `<a href="${href}"${rel}>${childrenText}</a>`
}, },
nodeTypes: [AutoLinkNode.getType()], nodeTypes: [AutoLinkNode.getType()],
} as HTMLConverter<SerializedAutoLinkNode>, },
},
hooks: {
afterRead: [
({ node }) => {
return node
},
],
}, },
node: AutoLinkNode, node: AutoLinkNode,
populationPromises: [linkPopulationPromiseHOC(props)], populationPromises: [linkPopulationPromiseHOC(props)],
}, }),
{ createNode({
converters: { converters: {
html: { html: {
converter: async ({ converters, node, parent, payload }) => { converter: async ({ converters, node, parent, payload }) => {
@@ -152,11 +164,11 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
return `<a href="${href}"${rel}>${childrenText}</a>` return `<a href="${href}"${rel}>${childrenText}</a>`
}, },
nodeTypes: [LinkNode.getType()], nodeTypes: [LinkNode.getType()],
} as HTMLConverter<SerializedLinkNode>, },
}, },
node: LinkNode, node: LinkNode,
populationPromises: [linkPopulationPromiseHOC(props)], populationPromises: [linkPopulationPromiseHOC(props)],
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -1,19 +1,22 @@
import { sanitizeFields } from 'payload/config'
import { deepCopyObject } from 'payload/utilities'
import type { PopulationPromise } from '../types.js' import type { PopulationPromise } from '../types.js'
import type { LinkFeatureServerProps } from './feature.server.js' import type { LinkFeatureServerProps } from './feature.server.js'
import type { SerializedLinkNode } from './nodes/types.js' import type { SerializedLinkNode } from './nodes/types.js'
import { populate } from '../../../populate/populate.js'
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js' import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
export const linkPopulationPromiseHOC = ( export const linkPopulationPromiseHOC = (
props: LinkFeatureServerProps, props: LinkFeatureServerProps,
): PopulationPromise<SerializedLinkNode> => { ): PopulationPromise<SerializedLinkNode> => {
const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({ return ({
context, context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises, editorPopulationPromises,
field, fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
node, node,
@@ -21,53 +24,55 @@ export const linkPopulationPromiseHOC = (
populationPromises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] // Sanitize link's fields here. This is done here and not in the feature, because the payload config is available here
const payloadConfig = req.payload.config
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
if (node?.fields?.doc?.value && node?.fields?.doc?.relationTo) { const transformedFields = transformExtraFields(
const collection = req.payload.collections[node?.fields?.doc?.relationTo] deepCopyObject(props.fields),
payloadConfig,
req.i18n,
props.enabledCollections,
props.disabledCollections,
)
if (collection) { // TODO: Sanitize & transform ahead of time! On startup!
promises.push( const sanitizedFields = sanitizeFields({
populate({ config: payloadConfig,
id: fields: transformedFields,
typeof node?.fields?.doc?.value === 'object' requireFieldLevelRichTextEditor: true,
? node?.fields?.doc?.value?.id validRelationships,
: node?.fields?.doc?.value, })
collection,
currentDepth, if (!sanitizedFields?.length) {
data: node?.fields?.doc, return
depth,
field,
key: 'value',
overrideAccess,
req,
showHiddenFields,
}),
)
}
} }
if (Array.isArray(props.fields)) {
/**
* Should populate all fields, including the doc field (for internal links), as it's treated like a normal field
*/
if (Array.isArray(sanitizedFields)) {
recurseNestedFields({ recurseNestedFields({
context, context,
currentDepth, currentDepth,
data: node.fields || {}, data: {
fields: node.fields,
},
depth, depth,
editorPopulationPromises, editorPopulationPromises,
fields: props.fields, fieldPromises,
fields: sanitizedFields,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc: node.fields || {}, siblingDoc: {
fields: node.fields,
},
}) })
} }
return promises
} }
return linkPopulationPromise
} }

View File

@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
import type { FeatureProviderProviderServer } from '../../types.js' import type { FeatureProviderProviderServer } from '../../types.js'
import { createNode } from '../../typeUtilities.js'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
import { CheckListFeatureClientComponent } from './feature.client.js' import { CheckListFeatureClientComponent } from './feature.client.js'
import { CHECK_LIST } from './markdownTransformers.js' import { CHECK_LIST } from './markdownTransformers.js'
@@ -17,18 +18,18 @@ export const CheckListFeature: FeatureProviderProviderServer<undefined, undefine
featureProviderMap.has('unorderedlist') || featureProviderMap.has('orderedlist') featureProviderMap.has('unorderedlist') || featureProviderMap.has('orderedlist')
? [] ? []
: [ : [
{ createNode({
converters: { converters: {
html: ListHTMLConverter, html: ListHTMLConverter,
}, },
node: ListNode, node: ListNode,
}, }),
{ createNode({
converters: { converters: {
html: ListItemHTMLConverter, html: ListItemHTMLConverter,
}, },
node: ListItemNode, node: ListItemNode,
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
import type { FeatureProviderProviderServer } from '../../types.js' import type { FeatureProviderProviderServer } from '../../types.js'
import { createNode } from '../../typeUtilities.js'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
import { OrderedListFeatureClientComponent } from './feature.client.js' import { OrderedListFeatureClientComponent } from './feature.client.js'
import { ORDERED_LIST } from './markdownTransformer.js' import { ORDERED_LIST } from './markdownTransformer.js'
@@ -16,18 +17,18 @@ export const OrderedListFeature: FeatureProviderProviderServer<undefined, undefi
nodes: featureProviderMap.has('unorderedlist') nodes: featureProviderMap.has('unorderedlist')
? [] ? []
: [ : [
{ createNode({
converters: { converters: {
html: ListHTMLConverter, html: ListHTMLConverter,
}, },
node: ListNode, node: ListNode,
}, }),
{ createNode({
converters: { converters: {
html: ListItemHTMLConverter, html: ListItemHTMLConverter,
}, },
node: ListItemNode, node: ListItemNode,
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
import type { FeatureProviderProviderServer } from '../../types.js' import type { FeatureProviderProviderServer } from '../../types.js'
import { createNode } from '../../typeUtilities.js'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
import { UnorderedListFeatureClientComponent } from './feature.client.js' import { UnorderedListFeatureClientComponent } from './feature.client.js'
import { UNORDERED_LIST } from './markdownTransformer.js' import { UNORDERED_LIST } from './markdownTransformer.js'
@@ -16,18 +17,18 @@ export const UnorderedListFeature: FeatureProviderProviderServer<undefined, unde
ClientComponent: UnorderedListFeatureClientComponent, ClientComponent: UnorderedListFeatureClientComponent,
markdownTransformers: [UNORDERED_LIST], markdownTransformers: [UNORDERED_LIST],
nodes: [ nodes: [
{ createNode({
converters: { converters: {
html: ListHTMLConverter, html: ListHTMLConverter,
}, },
node: ListNode, node: ListNode,
}, }),
{ createNode({
converters: { converters: {
html: ListItemHTMLConverter, html: ListItemHTMLConverter,
}, },
node: ListItemNode, node: ListItemNode,
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -16,10 +16,8 @@ export const _UploadConverter: LexicalPluginNodeConverter = {
fields, fields,
format: (lexicalPluginNode as any)?.format || '', format: (lexicalPluginNode as any)?.format || '',
relationTo: (lexicalPluginNode as any)?.rawImagePayload?.relationTo, relationTo: (lexicalPluginNode as any)?.rawImagePayload?.relationTo,
value: { value: (lexicalPluginNode as any)?.rawImagePayload?.value?.id || '',
id: (lexicalPluginNode as any)?.rawImagePayload?.value?.id || '', version: 2,
},
version: 1,
} as const as SerializedUploadNode } as const as SerializedUploadNode
}, },
nodeTypes: ['upload'], nodeTypes: ['upload'],

View File

@@ -7,10 +7,8 @@ export const _SlateRelationshipConverter: SlateNodeConverter = {
type: 'relationship', type: 'relationship',
format: '', format: '',
relationTo: slateNode.relationTo, relationTo: slateNode.relationTo,
value: { value: slateNode?.value?.id || '',
id: slateNode?.value?.id || '', version: 2,
},
version: 1,
} as const as SerializedRelationshipNode } as const as SerializedRelationshipNode
}, },
nodeTypes: ['relationship'], nodeTypes: ['relationship'],

View File

@@ -10,10 +10,8 @@ export const _SlateUploadConverter: SlateNodeConverter = {
}, },
format: '', format: '',
relationTo: slateNode.relationTo, relationTo: slateNode.relationTo,
value: { value: slateNode.value?.id || '',
id: slateNode.value?.id || '', version: 2,
},
version: 1,
} as const as SerializedUploadNode } as const as SerializedUploadNode
}, },
nodeTypes: ['upload'], nodeTypes: ['upload'],

View File

@@ -15,28 +15,26 @@ import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCond
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands.js' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands.js'
const insertRelationship = ({ const insertRelationship = ({
id,
editor, editor,
relationTo, relationTo,
replaceNodeKey, replaceNodeKey,
value,
}: { }: {
editor: LexicalEditor editor: LexicalEditor
id: string
relationTo: string relationTo: string
replaceNodeKey: null | string replaceNodeKey: null | string
value: number | string
}) => { }) => {
if (!replaceNodeKey) { if (!replaceNodeKey) {
editor.dispatchCommand(INSERT_RELATIONSHIP_COMMAND, { editor.dispatchCommand(INSERT_RELATIONSHIP_COMMAND, {
relationTo, relationTo,
value: { value,
id,
},
}) })
} else { } else {
editor.update(() => { editor.update(() => {
const node = $getNodeByKey(replaceNodeKey) const node = $getNodeByKey(replaceNodeKey)
if (node) { if (node) {
node.replace($createRelationshipNode({ relationTo, value: { id } })) node.replace($createRelationshipNode({ relationTo, value }))
} }
}) })
} }
@@ -75,10 +73,10 @@ const RelationshipDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }
const onSelect = useCallback( const onSelect = useCallback(
({ collectionSlug, docID }) => { ({ collectionSlug, docID }) => {
insertRelationship({ insertRelationship({
id: docID,
editor, editor,
relationTo: collectionSlug, relationTo: collectionSlug,
replaceNodeKey, replaceNodeKey,
value: docID,
}) })
closeDrawer() closeDrawer()
}, },

View File

@@ -1,5 +1,6 @@
import type { FeatureProviderProviderServer } from '../types.js' import type { FeatureProviderProviderServer } from '../types.js'
import { createNode } from '../typeUtilities.js'
import { RelationshipFeatureClientComponent } from './feature.client.js' import { RelationshipFeatureClientComponent } from './feature.client.js'
import { RelationshipNode } from './nodes/RelationshipNode.js' import { RelationshipNode } from './nodes/RelationshipNode.js'
import { relationshipPopulationPromise } from './populationPromise.js' import { relationshipPopulationPromise } from './populationPromise.js'
@@ -36,11 +37,11 @@ export const RelationshipFeature: FeatureProviderProviderServer<
ClientComponent: RelationshipFeatureClientComponent, ClientComponent: RelationshipFeatureClientComponent,
clientFeatureProps: props, clientFeatureProps: props,
nodes: [ nodes: [
{ createNode({
node: RelationshipNode, node: RelationshipNode,
populationPromises: [relationshipPopulationPromise], populationPromises: [relationshipPopulationPromise],
// TODO: Add validation similar to upload // TODO: Add validation similar to upload
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -25,11 +25,7 @@ const RelationshipComponent = React.lazy(() =>
export type RelationshipData = { export type RelationshipData = {
relationTo: string relationTo: string
value: { value: number | string
// Actual relationship, populated in afterRead hook
[key: string]: unknown
id: string
}
} }
export type SerializedRelationshipNode = Spread<RelationshipData, SerializedDecoratorBlockNode> export type SerializedRelationshipNode = Spread<RelationshipData, SerializedDecoratorBlockNode>
@@ -41,9 +37,7 @@ function relationshipElementToNode(domNode: HTMLDivElement): DOMConversionOutput
if (id != null && relationTo != null) { if (id != null && relationTo != null) {
const node = $createRelationshipNode({ const node = $createRelationshipNode({
relationTo, relationTo,
value: { value: id,
id,
},
}) })
return { node } return { node }
} }
@@ -96,6 +90,10 @@ export class RelationshipNode extends DecoratorBlockNode {
} }
static importJSON(serializedNode: SerializedRelationshipNode): RelationshipNode { static importJSON(serializedNode: SerializedRelationshipNode): RelationshipNode {
if (serializedNode.version === 1 && (serializedNode?.value as unknown as { id: string })?.id) {
serializedNode.value = (serializedNode.value as unknown as { id: string }).id
}
const importedData: RelationshipData = { const importedData: RelationshipData = {
relationTo: serializedNode.relationTo, relationTo: serializedNode.relationTo,
value: serializedNode.value, value: serializedNode.value,
@@ -108,6 +106,7 @@ export class RelationshipNode extends DecoratorBlockNode {
static isInline(): false { static isInline(): false {
return false return false
} }
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element { decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
return ( return (
<RelationshipComponent <RelationshipComponent
@@ -118,10 +117,9 @@ export class RelationshipNode extends DecoratorBlockNode {
/> />
) )
} }
exportDOM(): DOMExportOutput { exportDOM(): DOMExportOutput {
const element = document.createElement('div') const element = document.createElement('div')
element.setAttribute('data-lexical-relationship-id', this.__data?.value?.id) element.setAttribute('data-lexical-relationship-id', String(this.__data?.value))
element.setAttribute('data-lexical-relationship-relationTo', this.__data?.relationTo) element.setAttribute('data-lexical-relationship-relationTo', this.__data?.relationTo)
const text = document.createTextNode(this.getTextContent()) const text = document.createTextNode(this.getTextContent())
@@ -134,7 +132,7 @@ export class RelationshipNode extends DecoratorBlockNode {
...super.exportJSON(), ...super.exportJSON(),
...this.getData(), ...this.getData(),
type: this.getType(), type: this.getType(),
version: 1, version: 2,
} }
} }
@@ -143,7 +141,7 @@ export class RelationshipNode extends DecoratorBlockNode {
} }
getTextContent(): string { getTextContent(): string {
return `${this.__data?.relationTo} relation to ${this.__data?.value?.id}` return `${this.__data?.relationTo} relation to ${this.__data?.value}`
} }
setData(data: RelationshipData): void { setData(data: RelationshipData): void {

View File

@@ -38,10 +38,7 @@ type Props = {
const Component: React.FC<Props> = (props) => { const Component: React.FC<Props> = (props) => {
const { const {
children, children,
data: { data: { relationTo, value: id },
relationTo,
value: { id },
},
nodeKey, nodeKey,
} = props } = props

View File

@@ -9,18 +9,20 @@ export const relationshipPopulationPromise: PopulationPromise<SerializedRelation
field, field,
node, node,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) => { }) => {
const promises: Promise<void>[] = [] if (node?.value) {
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
if (node?.value?.id) {
const collection = req.payload.collections[node?.relationTo] const collection = req.payload.collections[node?.relationTo]
if (collection) { if (collection) {
promises.push( populationPromises.push(
populate({ populate({
id: node.value.id, id,
collection, collection,
currentDepth, currentDepth,
data: node, data: node,
@@ -34,6 +36,4 @@ export const relationshipPopulationPromise: PopulationPromise<SerializedRelation
) )
} }
} }
return promises
} }

View File

@@ -0,0 +1,13 @@
import type { LexicalNode } from 'lexical'
import type { NodeWithHooks } from './types.js'
/**
* Utility function to create a node with hooks. You don't have to use this utility, but it improves type inference
* @param node the node
*/
export function createNode<Node extends LexicalNode>(
node: NodeWithHooks<Node>,
): NodeWithHooks<Node> {
return node
}

View File

@@ -6,7 +6,13 @@ import type { SerializedLexicalNode } from 'lexical'
import type { LexicalNodeReplacement } from 'lexical' import type { LexicalNodeReplacement } from 'lexical'
import type { RequestContext } from 'payload' import type { RequestContext } from 'payload'
import type { SanitizedConfig } from 'payload/config' import type { SanitizedConfig } from 'payload/config'
import type { Field, PayloadRequest, RichTextField, ValidateOptions } from 'payload/types' import type {
Field,
PayloadRequest,
ReplaceAny,
RichTextField,
ValidateOptions,
} from 'payload/types'
import type React from 'react' import type React from 'react'
import type { AdapterProps } from '../../types.js' import type { AdapterProps } from '../../types.js'
@@ -21,6 +27,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
depth, depth,
editorPopulationPromises, editorPopulationPromises,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
node, node,
@@ -38,6 +45,10 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
*/ */
editorPopulationPromises: Map<string, Array<PopulationPromise>> editorPopulationPromises: Map<string, Array<PopulationPromise>>
field: RichTextField<SerializedEditorState, AdapterProps> field: RichTextField<SerializedEditorState, AdapterProps>
/**
* fieldPromises are used for things like field hooks. They will be awaited before awaiting populationPromises
*/
fieldPromises: Promise<void>[]
findMany: boolean findMany: boolean
flattenLocales: boolean flattenLocales: boolean
node: T node: T
@@ -46,7 +57,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>
}) => Promise<void>[] }) => void
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({ export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
node, node,
@@ -171,6 +182,44 @@ export type ClientComponentProps<ClientFeatureProps> = ClientFeatureProps & {
order: number order: number
} }
export type FieldNodeHookArgs<T extends SerializedLexicalNode> = {
context: RequestContext
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
findMany?: boolean
/** The value of the field. */
node?: T
/** A string relating to which operation the field type is currently executing within. Useful within beforeValidate, beforeChange, and afterChange hooks to differentiate between create and update operations. */
operation?: 'create' | 'delete' | 'read' | 'update'
/** The Express request object. It is mocked for Local API operations. */
req: PayloadRequest
}
export type FieldNodeHook<T extends SerializedLexicalNode> = (
args: FieldNodeHookArgs<T>,
) => Promise<T> | T
// Define the node with hooks that use the node's exportJSON return type
export type NodeWithHooks<T extends LexicalNode = any> = {
converters?: {
html?: HTMLConverter<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
}
hooks?: {
afterChange?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
afterRead?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
beforeChange?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
/**
* Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
*/
beforeDuplicate?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
beforeValidate?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
}
node: Klass<T> | LexicalNodeReplacement
populationPromises?: Array<
PopulationPromise<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
>
validations?: Array<NodeValidation<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
}
export type ServerFeature<ServerProps, ClientFeatureProps> = { export type ServerFeature<ServerProps, ClientFeatureProps> = {
ClientComponent?: React.FC<ClientComponentProps<ClientFeatureProps>> ClientComponent?: React.FC<ClientComponentProps<ClientFeatureProps>>
/** /**
@@ -215,26 +264,8 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
isRequired: boolean isRequired: boolean
}) => JSONSchema4 }) => JSONSchema4
} }
hooks?: {
afterReadPromise?: ({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<SerializedEditorState, AdapterProps>
incomingEditorState: SerializedEditorState
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
}
markdownTransformers?: Transformer[] markdownTransformers?: Transformer[]
nodes?: Array<{ nodes?: Array<NodeWithHooks>
converters?: {
html?: HTMLConverter
}
node: Klass<LexicalNode> | LexicalNodeReplacement
populationPromises?: Array<PopulationPromise>
validations?: Array<NodeValidation>
}>
/** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */ /** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */
serverFeatureProps: ServerProps serverFeatureProps: ServerProps
@@ -325,20 +356,18 @@ export type SanitizedServerFeatures = Required<
}) => JSONSchema4 }) => JSONSchema4
> >
} }
hooks: { /** The node types mapped to their hooks */
afterReadPromises: Array<
({ hooks?: {
field, afterChange?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
incomingEditorState, afterRead?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
siblingDoc, beforeChange?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
}: { /**
field: RichTextField<SerializedEditorState, AdapterProps> * Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
incomingEditorState: SerializedEditorState */
siblingDoc: Record<string, unknown> beforeDuplicate?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
}) => Promise<void> | null beforeValidate?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
> } /** The node types mapped to their populationPromises */
}
/** The node types mapped to their populationPromises */
populationPromises: Map<string, Array<PopulationPromise>> populationPromises: Map<string, Array<PopulationPromise>>
/** The node types mapped to their validations */ /** The node types mapped to their validations */
validations: Map<string, Array<NodeValidation>> validations: Map<string, Array<NodeValidation>>

View File

@@ -65,13 +65,13 @@ const Component: React.FC<ElementProps> = (props) => {
const drawerSlug = useDrawerSlug('upload-drawer') const drawerSlug = useDrawerSlug('upload-drawer')
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({ const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({
id: value?.id, id: value,
collectionSlug: relatedCollection.slug, collectionSlug: relatedCollection.slug,
}) })
// Get the referenced document // Get the referenced document
const [{ data }, { setParams }] = usePayloadAPI( const [{ data }, { setParams }] = usePayloadAPI(
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`, `${serverURL}${api}/${relatedCollection.slug}/${value}`,
{ initialParams }, { initialParams },
) )
@@ -172,7 +172,7 @@ const Component: React.FC<ElementProps> = (props) => {
</DocumentDrawerToggler> </DocumentDrawerToggler>
</div> </div>
</div> </div>
{value?.id && <DocumentDrawer onSave={updateUpload} />} {value && <DocumentDrawer onSave={updateUpload} />}
{hasExtraFields ? ( {hasExtraFields ? (
<ExtraFieldsUploadDrawer <ExtraFieldsUploadDrawer
drawerSlug={drawerSlug} drawerSlug={drawerSlug}

View File

@@ -17,21 +17,21 @@ import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands.js'
const baseClass = 'lexical-upload-drawer' const baseClass = 'lexical-upload-drawer'
const insertUpload = ({ const insertUpload = ({
id,
editor, editor,
relationTo, relationTo,
replaceNodeKey, replaceNodeKey,
value,
}: { }: {
editor: LexicalEditor editor: LexicalEditor
id: string
relationTo: string relationTo: string
replaceNodeKey: null | string replaceNodeKey: null | string
value: number | string
}) => { }) => {
if (!replaceNodeKey) { if (!replaceNodeKey) {
editor.dispatchCommand(INSERT_UPLOAD_COMMAND, { editor.dispatchCommand(INSERT_UPLOAD_COMMAND, {
id,
fields: null, fields: null,
relationTo, relationTo,
value,
}) })
} else { } else {
editor.update(() => { editor.update(() => {
@@ -42,9 +42,7 @@ const insertUpload = ({
data: { data: {
fields: null, fields: null,
relationTo, relationTo,
value: { value,
id,
},
}, },
}), }),
) )
@@ -84,10 +82,10 @@ const UploadDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }) => {
const onSelect = useCallback( const onSelect = useCallback(
({ collectionSlug, docID }) => { ({ collectionSlug, docID }) => {
insertUpload({ insertUpload({
id: docID,
editor, editor,
relationTo: collectionSlug, relationTo: collectionSlug,
replaceNodeKey, replaceNodeKey,
value: docID,
}) })
closeDrawer() closeDrawer()
}, },

View File

@@ -1,13 +1,20 @@
import type { Field, FieldWithRichTextRequiredEditor, Payload } from 'payload/types' import type {
Field,
FieldWithRichTextRequiredEditor,
FileData,
FileSize,
Payload,
TypeWithID,
} from 'payload/types'
import { traverseFields } from '@payloadcms/next/utilities' import { traverseFields } from '@payloadcms/next/utilities'
import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js' import type { FeatureProviderProviderServer } from '../types.js'
import type { UploadFeaturePropsClient } from './feature.client.js' import type { UploadFeaturePropsClient } from './feature.client.js'
import { createNode } from '../typeUtilities.js'
import { UploadFeatureClientComponent } from './feature.client.js' import { UploadFeatureClientComponent } from './feature.client.js'
import { type SerializedUploadNode, UploadNode } from './nodes/UploadNode.js' import { UploadNode } from './nodes/UploadNode.js'
import { uploadPopulationPromiseHOC } from './populationPromise.js' import { uploadPopulationPromiseHOC } from './populationPromise.js'
import { uploadValidation } from './validate.js' import { uploadValidation } from './validate.js'
@@ -71,17 +78,21 @@ export const UploadFeature: FeatureProviderProviderServer<
return schemaMap return schemaMap
}, },
nodes: [ nodes: [
{ createNode({
converters: { converters: {
html: { html: {
converter: async ({ node, payload }) => { converter: async ({ node, payload }) => {
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
if (payload) { if (payload) {
let uploadDocument: any let uploadDocument: TypeWithID & FileData
try { try {
uploadDocument = await payload.findByID({ uploadDocument = (await payload.findByID({
id: node.value.id, id,
collection: node.relationTo, collection: node.relationTo,
}) })) as TypeWithID & FileData
} catch (ignored) { } catch (ignored) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error( console.error(
@@ -93,12 +104,12 @@ export const UploadFeature: FeatureProviderProviderServer<
return `<img />` return `<img />`
} }
const url: string = getAbsoluteURL(uploadDocument?.url as string, payload) const url = getAbsoluteURL(uploadDocument?.url, payload)
/** /**
* If the upload is not an image, return a link to the upload * If the upload is not an image, return a link to the upload
*/ */
if (!(uploadDocument?.mimeType as string)?.startsWith('image')) { if (!uploadDocument?.mimeType?.startsWith('image')) {
return `<a href="${url}" rel="noopener noreferrer">${uploadDocument.filename}</a>` return `<a href="${url}" rel="noopener noreferrer">${uploadDocument.filename}</a>`
} }
@@ -116,7 +127,9 @@ export const UploadFeature: FeatureProviderProviderServer<
// Iterate through each size in the data.sizes object // Iterate through each size in the data.sizes object
for (const size in uploadDocument.sizes) { for (const size in uploadDocument.sizes) {
const imageSize = uploadDocument.sizes[size] const imageSize: FileSize & {
url?: string
} = uploadDocument.sizes[size]
// Skip if any property of the size object is null // Skip if any property of the size object is null
if ( if (
@@ -129,7 +142,7 @@ export const UploadFeature: FeatureProviderProviderServer<
) { ) {
continue continue
} }
const imageSizeURL: string = getAbsoluteURL(imageSize?.url as string, payload) const imageSizeURL = getAbsoluteURL(imageSize?.url, payload)
pictureHTML += `<source srcset="${imageSizeURL}" media="(max-width: ${imageSize.width}px)" type="${imageSize.mimeType}">` pictureHTML += `<source srcset="${imageSizeURL}" media="(max-width: ${imageSize.width}px)" type="${imageSize.mimeType}">`
} }
@@ -139,16 +152,16 @@ export const UploadFeature: FeatureProviderProviderServer<
pictureHTML += '</picture>' pictureHTML += '</picture>'
return pictureHTML return pictureHTML
} else { } else {
return `<img src="${node.value.id}" />` return `<img src="${id}" />`
} }
}, },
nodeTypes: [UploadNode.getType()], nodeTypes: [UploadNode.getType()],
} as HTMLConverter<SerializedUploadNode>, },
}, },
node: UploadNode, node: UploadNode,
populationPromises: [uploadPopulationPromiseHOC(props)], populationPromises: [uploadPopulationPromiseHOC(props)],
validations: [uploadValidation()], validations: [uploadValidation()],
}, }),
], ],
serverFeatureProps: props, serverFeatureProps: props,
} }

View File

@@ -21,26 +21,13 @@ const RawUploadComponent = React.lazy(() =>
import('../component/index.js').then((module) => ({ default: module.UploadComponent })), import('../component/index.js').then((module) => ({ default: module.UploadComponent })),
) )
export type RawUploadPayload = {
fields: {
// unknown, custom fields:
[key: string]: unknown
}
id: string
relationTo: string
}
export type UploadData = { export type UploadData = {
fields: { fields: {
// unknown, custom fields: // unknown, custom fields:
[key: string]: unknown [key: string]: unknown
} }
relationTo: string relationTo: string
value: { value: number | string
// Actual upload data, populated in afterRead hook
[key: string]: unknown
id: string
}
} }
function convertUploadElement(domNode: Node): DOMConversionOutput | null { function convertUploadElement(domNode: Node): DOMConversionOutput | null {
@@ -93,6 +80,10 @@ export class UploadNode extends DecoratorBlockNode {
} }
static importJSON(serializedNode: SerializedUploadNode): UploadNode { static importJSON(serializedNode: SerializedUploadNode): UploadNode {
if (serializedNode.version === 1 && (serializedNode?.value as unknown as { id: string })?.id) {
serializedNode.value = (serializedNode.value as unknown as { id: string }).id
}
const importedData: UploadData = { const importedData: UploadData = {
fields: serializedNode.fields, fields: serializedNode.fields,
relationTo: serializedNode.relationTo, relationTo: serializedNode.relationTo,
@@ -126,7 +117,7 @@ export class UploadNode extends DecoratorBlockNode {
...super.exportJSON(), ...super.exportJSON(),
...this.getData(), ...this.getData(),
type: this.getType(), type: this.getType(),
version: 1, version: 2,
} }
} }

View File

@@ -18,12 +18,12 @@ import type { LexicalCommand } from 'lexical'
import { useConfig } from '@payloadcms/ui/providers/Config' import { useConfig } from '@payloadcms/ui/providers/Config'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import type { RawUploadPayload } from '../nodes/UploadNode.js' import type { UploadData } from '../nodes/UploadNode.js'
import { UploadDrawer } from '../drawer/index.js' import { UploadDrawer } from '../drawer/index.js'
import { $createUploadNode, UploadNode } from '../nodes/UploadNode.js' import { $createUploadNode, UploadNode } from '../nodes/UploadNode.js'
export type InsertUploadPayload = Readonly<RawUploadPayload> export type InsertUploadPayload = Readonly<UploadData>
export const INSERT_UPLOAD_COMMAND: LexicalCommand<InsertUploadPayload> = export const INSERT_UPLOAD_COMMAND: LexicalCommand<InsertUploadPayload> =
createCommand('INSERT_UPLOAD_COMMAND') createCommand('INSERT_UPLOAD_COMMAND')
@@ -46,9 +46,7 @@ export function UploadPlugin(): JSX.Element | null {
data: { data: {
fields: payload.fields, fields: payload.fields,
relationTo: payload.relationTo, relationTo: payload.relationTo,
value: { value: payload.value,
id: payload.id,
},
}, },
}) })

View File

@@ -1,3 +1,5 @@
import { sanitizeFields } from 'payload/config'
import type { PopulationPromise } from '../types.js' import type { PopulationPromise } from '../types.js'
import type { UploadFeatureProps } from './feature.server.js' import type { UploadFeatureProps } from './feature.server.js'
import type { SerializedUploadNode } from './nodes/UploadNode.js' import type { SerializedUploadNode } from './nodes/UploadNode.js'
@@ -8,12 +10,13 @@ import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
export const uploadPopulationPromiseHOC = ( export const uploadPopulationPromiseHOC = (
props?: UploadFeatureProps, props?: UploadFeatureProps,
): PopulationPromise<SerializedUploadNode> => { ): PopulationPromise<SerializedUploadNode> => {
const uploadPopulationPromise: PopulationPromise<SerializedUploadNode> = ({ return ({
context, context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises, editorPopulationPromises,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
node, node,
@@ -21,17 +24,19 @@ export const uploadPopulationPromiseHOC = (
populationPromises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] const payloadConfig = req.payload.config
if (node?.value?.id) { if (node?.value) {
const collection = req.payload.collections[node?.relationTo] const collection = req.payload.collections[node?.relationTo]
if (collection) { if (collection) {
promises.push( // @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
populationPromises.push(
populate({ populate({
id: node?.value?.id, id,
collection, collection,
currentDepth, currentDepth,
data: node, data: node,
@@ -45,27 +50,36 @@ export const uploadPopulationPromiseHOC = (
) )
} }
if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) { if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
// TODO: Sanitize & transform ahead of time! On startup!
const sanitizedFields = sanitizeFields({
config: payloadConfig,
fields: props?.collections?.[node?.relationTo]?.fields,
requireFieldLevelRichTextEditor: true,
validRelationships,
})
if (!sanitizedFields?.length) {
return
}
recurseNestedFields({ recurseNestedFields({
context, context,
currentDepth, currentDepth,
data: node.fields || {}, data: node.fields || {},
depth, depth,
editorPopulationPromises, editorPopulationPromises,
fields: props?.collections?.[node?.relationTo]?.fields, fieldPromises,
fields: sanitizedFields,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc: node.fields || {}, siblingDoc: node.fields || {},
}) })
} }
} }
return promises
} }
return uploadPopulationPromise
} }

View File

@@ -6,7 +6,7 @@ import type { SerializedUploadNode } from './nodes/UploadNode.js'
import { CAN_USE_DOM } from '../../lexical/utils/canUseDOM.js' import { CAN_USE_DOM } from '../../lexical/utils/canUseDOM.js'
export const uploadValidation = (): NodeValidation<SerializedUploadNode> => { export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
const uploadValidation: NodeValidation<SerializedUploadNode> = ({ return ({
node, node,
validation: { validation: {
options: { options: {
@@ -16,8 +16,10 @@ export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
}) => { }) => {
if (!CAN_USE_DOM) { if (!CAN_USE_DOM) {
const idType = payload.collections[node.relationTo].customIDType || payload.db.defaultIDType const idType = payload.collections[node.relationTo].customIDType || payload.db.defaultIDType
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
if (!isValidID(node.value?.id, idType)) { if (!isValidID(id, idType)) {
return t('validation:validUploadID') return t('validation:validUploadID')
} }
} }
@@ -26,6 +28,4 @@ export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
return true return true
} }
return uploadValidation
} }

View File

@@ -16,7 +16,11 @@ export const sanitizeServerFeatures = (
modifyOutputSchemas: [], modifyOutputSchemas: [],
}, },
hooks: { hooks: {
afterReadPromises: [], afterChange: new Map(),
afterRead: new Map(),
beforeChange: new Map(),
beforeDuplicate: new Map(),
beforeValidate: new Map(),
}, },
markdownTransformers: [], markdownTransformers: [],
nodes: [], nodes: [],
@@ -33,13 +37,6 @@ export const sanitizeServerFeatures = (
if (feature?.generatedTypes?.modifyOutputSchema) { if (feature?.generatedTypes?.modifyOutputSchema) {
sanitized.generatedTypes.modifyOutputSchemas.push(feature.generatedTypes.modifyOutputSchema) sanitized.generatedTypes.modifyOutputSchemas.push(feature.generatedTypes.modifyOutputSchema)
} }
if (feature.hooks) {
if (feature.hooks.afterReadPromise) {
sanitized.hooks.afterReadPromises = sanitized.hooks.afterReadPromises.concat(
feature.hooks.afterReadPromise,
)
}
}
if (feature.nodes?.length) { if (feature.nodes?.length) {
sanitized.nodes = sanitized.nodes.concat(feature.nodes) sanitized.nodes = sanitized.nodes.concat(feature.nodes)
@@ -54,6 +51,21 @@ export const sanitizeServerFeatures = (
if (node?.converters?.html) { if (node?.converters?.html) {
sanitized.converters.html.push(node.converters.html) sanitized.converters.html.push(node.converters.html)
} }
if (node?.hooks?.afterChange) {
sanitized.hooks.afterChange.set(nodeType, node.hooks.afterChange)
}
if (node?.hooks?.afterRead) {
sanitized.hooks.afterRead.set(nodeType, node.hooks.afterRead)
}
if (node?.hooks?.beforeChange) {
sanitized.hooks.beforeChange.set(nodeType, node.hooks.beforeChange)
}
if (node?.hooks?.beforeDuplicate) {
sanitized.hooks.beforeDuplicate.set(nodeType, node.hooks.beforeDuplicate)
}
if (node?.hooks?.beforeValidate) {
sanitized.hooks.beforeValidate.set(nodeType, node.hooks.beforeValidate)
}
}) })
} }

View File

@@ -21,6 +21,8 @@ export function validateUrl(url: string): boolean {
// TODO Fix UI for link insertion; it should never default to an invalid URL such as https://. // TODO Fix UI for link insertion; it should never default to an invalid URL such as https://.
// Maybe show a dialog where they user can type the URL before inserting it. // Maybe show a dialog where they user can type the URL before inserting it.
if (!url) return false
if (url === 'https://') return true if (url === 'https://') return true
// This makes sure URLs starting with www. instead of https are valid too // This makes sure URLs starting with www. instead of https are valid too

View File

@@ -20,7 +20,7 @@ import { sanitizeServerFeatures } from './field/lexical/config/server/sanitize.j
import { cloneDeep } from './field/lexical/utils/cloneDeep.js' import { cloneDeep } from './field/lexical/utils/cloneDeep.js'
import { getGenerateComponentMap } from './generateComponentMap.js' import { getGenerateComponentMap } from './generateComponentMap.js'
import { getGenerateSchemaMap } from './generateSchemaMap.js' import { getGenerateSchemaMap } from './generateSchemaMap.js'
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise.js' import { populateLexicalPopulationPromises } from './populate/populateLexicalPopulationPromises.js'
import { richTextValidateHOC } from './validate/index.js' import { richTextValidateHOC } from './validate/index.js'
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter { export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
@@ -64,29 +64,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
Component: RichTextField, Component: RichTextField,
toMergeIntoProps: { lexicalEditorConfig: finalSanitizedEditorConfig.lexical }, toMergeIntoProps: { lexicalEditorConfig: finalSanitizedEditorConfig.lexical },
}), }),
afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
return new Promise<void>((resolve, reject) => {
const promises: Promise<void>[] = []
if (finalSanitizedEditorConfig?.features?.hooks?.afterReadPromises?.length) {
for (const afterReadPromise of finalSanitizedEditorConfig.features.hooks
.afterReadPromises) {
const promise = afterReadPromise({
field,
incomingEditorState,
siblingDoc,
})
if (promise) {
promises.push(promise)
}
}
}
Promise.all(promises)
.then(() => resolve())
.catch((error) => reject(error))
})
},
editorConfig: finalSanitizedEditorConfig, editorConfig: finalSanitizedEditorConfig,
generateComponentMap: getGenerateComponentMap({ generateComponentMap: getGenerateComponentMap({
resolvedFeatureMap, resolvedFeatureMap,
@@ -94,6 +71,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
generateSchemaMap: getGenerateSchemaMap({ generateSchemaMap: getGenerateSchemaMap({
resolvedFeatureMap, resolvedFeatureMap,
}), }),
/* hooks: {
afterChange: finalSanitizedEditorConfig.features.hooks.afterChange,
afterRead: finalSanitizedEditorConfig.features.hooks.afterRead,
beforeChange: finalSanitizedEditorConfig.features.hooks.beforeChange,
beforeDuplicate: finalSanitizedEditorConfig.features.hooks.beforeDuplicate,
beforeValidate: finalSanitizedEditorConfig.features.hooks.beforeValidate,
},*/
outputSchema: ({ outputSchema: ({
collectionIDFieldTypes, collectionIDFieldTypes,
config, config,
@@ -171,11 +155,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
return outputSchema return outputSchema
}, },
populationPromise({ populationPromises({
context, context,
currentDepth, currentDepth,
depth, depth,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
@@ -186,12 +171,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}) { }) {
// check if there are any features with nodes which have populationPromises for this field // check if there are any features with nodes which have populationPromises for this field
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) { if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
return richTextRelationshipPromise({ populateLexicalPopulationPromises({
context, context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises, editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
@@ -201,8 +187,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
siblingDoc, siblingDoc,
}) })
} }
return null
}, },
validate: richTextValidateHOC({ validate: richTextValidateHOC({
editorConfig: finalSanitizedEditorConfig, editorConfig: finalSanitizedEditorConfig,
@@ -311,14 +295,19 @@ export {
RelationshipNode, RelationshipNode,
type SerializedRelationshipNode, type SerializedRelationshipNode,
} from './field/features/relationship/nodes/RelationshipNode.js' } from './field/features/relationship/nodes/RelationshipNode.js'
export { createNode } from './field/features/typeUtilities.js'
export type { export type {
ClientComponentProps,
ClientFeature, ClientFeature,
ClientFeatureProviderMap, ClientFeatureProviderMap,
FeatureProviderClient, FeatureProviderClient,
FeatureProviderProviderClient, FeatureProviderProviderClient,
FeatureProviderProviderServer, FeatureProviderProviderServer,
FeatureProviderServer, FeatureProviderServer,
FieldNodeHook,
FieldNodeHookArgs,
NodeValidation, NodeValidation,
NodeWithHooks,
PopulationPromise, PopulationPromise,
ResolvedClientFeature, ResolvedClientFeature,
ResolvedClientFeatureMap, ResolvedClientFeatureMap,
@@ -334,8 +323,6 @@ export { UploadFeature } from './field/features/upload/feature.server.js'
export type { UploadFeatureProps } from './field/features/upload/feature.server.js' export type { UploadFeatureProps } from './field/features/upload/feature.server.js'
export type { RawUploadPayload } from './field/features/upload/nodes/UploadNode.js'
export { export {
$createUploadNode, $createUploadNode,
$isUploadNode, $isUploadNode,

View File

@@ -28,7 +28,7 @@ export const populate = async ({
}: Omit<Arguments, 'field'> & { }: Omit<Arguments, 'field'> & {
collection: Collection collection: Collection
field: Field field: Field
id: string id: number | string
}): Promise<void> => { }): Promise<void> => {
const dataRef = data as Record<string, unknown> const dataRef = data as Record<string, unknown>

View File

@@ -1,26 +1,17 @@
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical' import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
import type { PayloadRequest, RichTextAdapter, RichTextField } from 'payload/types' import type { RichTextAdapter } from 'payload/types'
import type { PopulationPromise } from '../field/features/types.js' import type { PopulationPromise } from '../field/features/types.js'
import type { AdapterProps } from '../types.js' import type { AdapterProps } from '../types.js'
export type Args = Parameters< export type Args = Parameters<
RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromise'] RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromises']
>[0] & { >[0] & {
editorPopulationPromises: Map<string, Array<PopulationPromise>> editorPopulationPromises: Map<string, Array<PopulationPromise>>
} }
type RecurseRichTextArgs = { type RecurseRichTextArgs = {
children: SerializedLexicalNode[] children: SerializedLexicalNode[]
currentDepth: number
depth: number
editorPopulationPromises: Map<string, Array<PopulationPromise>>
field: RichTextField<SerializedEditorState, AdapterProps>
overrideAccess: boolean
promises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
siblingDoc?: Record<string, unknown>
} }
export const recurseRichText = ({ export const recurseRichText = ({
@@ -30,15 +21,15 @@ export const recurseRichText = ({
depth, depth,
editorPopulationPromises, editorPopulationPromises,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess = false, overrideAccess = false,
populationPromises, populationPromises,
promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}: RecurseRichTextArgs & Args): void => { }: Args & RecurseRichTextArgs): void => {
if (depth <= 0 || currentDepth > depth) { if (depth <= 0 || currentDepth > depth) {
return return
} }
@@ -47,23 +38,22 @@ export const recurseRichText = ({
children.forEach((node) => { children.forEach((node) => {
if (editorPopulationPromises?.has(node.type)) { if (editorPopulationPromises?.has(node.type)) {
for (const promise of editorPopulationPromises.get(node.type)) { for (const promise of editorPopulationPromises.get(node.type)) {
promises.push( promise({
...promise({ context,
context, currentDepth,
currentDepth, depth,
depth, editorPopulationPromises,
editorPopulationPromises, field,
field, fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
node, node,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}), })
)
} }
} }
@@ -75,11 +65,11 @@ export const recurseRichText = ({
depth, depth,
editorPopulationPromises, editorPopulationPromises,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
@@ -89,12 +79,16 @@ export const recurseRichText = ({
} }
} }
export const richTextRelationshipPromise = async ({ /**
* Appends all new populationPromises to the populationPromises prop
*/
export const populateLexicalPopulationPromises = ({
context, context,
currentDepth, currentDepth,
depth, depth,
editorPopulationPromises, editorPopulationPromises,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
@@ -102,9 +96,7 @@ export const richTextRelationshipPromise = async ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}: Args): Promise<void> => { }: Args) => {
const promises = []
recurseRichText({ recurseRichText({
children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [], children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [],
context, context,
@@ -112,15 +104,13 @@ export const richTextRelationshipPromise = async ({
depth, depth,
editorPopulationPromises, editorPopulationPromises,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
populationPromises, populationPromises,
promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}) })
await Promise.all(promises)
} }

View File

@@ -14,12 +14,15 @@ type NestedRichTextFieldsArgs = {
* This maps all the population promises to the node types * This maps all the population promises to the node types
*/ */
editorPopulationPromises: Map<string, Array<PopulationPromise>> editorPopulationPromises: Map<string, Array<PopulationPromise>>
/**
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
*/
fieldPromises: Promise<void>[]
fields: Field[] fields: Field[]
findMany: boolean findMany: boolean
flattenLocales: boolean flattenLocales: boolean
overrideAccess: boolean overrideAccess: boolean
populationPromises: Promise<void>[] populationPromises: Promise<void>[]
promises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>
@@ -30,12 +33,12 @@ export const recurseNestedFields = ({
currentDepth = 0, currentDepth = 0,
data, data,
depth, depth,
fieldPromises,
fields, fields,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess = false, overrideAccess = false,
populationPromises, populationPromises,
promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
@@ -47,7 +50,7 @@ export const recurseNestedFields = ({
depth, 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 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
fallbackLocale: req.fallbackLocale, fallbackLocale: req.fallbackLocale,
fieldPromises: promises, // Not sure if what I pass in here makes sense. But it doesn't seem like it's used at all anyways fieldPromises,
fields, fields,
findMany, findMany,
flattenLocales, flattenLocales,
@@ -58,7 +61,7 @@ export const recurseNestedFields = ({
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
triggerAccessControl: false, // TODO: Enable this to support access control //triggerAccessControl: false, // TODO: Enable this to support access control
triggerHooks: false, // TODO: Enable this to support hooks //triggerHooks: false, // TODO: Enable this to support hooks
}) })
} }

View File

@@ -11,7 +11,7 @@ type NestedRichTextFieldsArgs = {
depth: number depth: number
fields: Field[] fields: Field[]
overrideAccess: boolean overrideAccess: boolean
promises: Promise<void>[] populationPromises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
} }
@@ -22,7 +22,7 @@ export const recurseNestedFields = ({
depth, depth,
fields, fields,
overrideAccess = false, overrideAccess = false,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}: NestedRichTextFieldsArgs): void => { }: NestedRichTextFieldsArgs): void => {
@@ -34,7 +34,7 @@ export const recurseNestedFields = ({
data[field.name].forEach(({ relationTo, value }, i) => { data[field.name].forEach(({ relationTo, value }, i) => {
const collection = req.payload.collections[relationTo] const collection = req.payload.collections[relationTo]
if (collection) { if (collection) {
promises.push( populationPromises.push(
populate({ populate({
id: value, id: value,
collection, collection,
@@ -54,7 +54,7 @@ export const recurseNestedFields = ({
data[field.name].forEach((id, i) => { data[field.name].forEach((id, i) => {
const collection = req.payload.collections[field.relationTo as string] const collection = req.payload.collections[field.relationTo as string]
if (collection) { if (collection) {
promises.push( populationPromises.push(
populate({ populate({
id, id,
collection, collection,
@@ -78,7 +78,7 @@ export const recurseNestedFields = ({
) { ) {
if (!('hasMany' in field) || !field.hasMany) { if (!('hasMany' in field) || !field.hasMany) {
const collection = req.payload.collections[data[field.name].relationTo] const collection = req.payload.collections[data[field.name].relationTo]
promises.push( populationPromises.push(
populate({ populate({
id: data[field.name].value, id: data[field.name].value,
collection, collection,
@@ -97,7 +97,7 @@ export const recurseNestedFields = ({
} }
if (typeof data[field.name] !== 'undefined' && typeof field.relationTo === 'string') { if (typeof data[field.name] !== 'undefined' && typeof field.relationTo === 'string') {
const collection = req.payload.collections[field.relationTo] const collection = req.payload.collections[field.relationTo]
promises.push( populationPromises.push(
populate({ populate({
id: data[field.name], id: data[field.name],
collection, collection,
@@ -120,7 +120,7 @@ export const recurseNestedFields = ({
depth, depth,
fields: field.fields, fields: field.fields,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
@@ -131,7 +131,7 @@ export const recurseNestedFields = ({
depth, depth,
fields: field.fields, fields: field.fields,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
@@ -144,7 +144,7 @@ export const recurseNestedFields = ({
depth, depth,
fields: tab.fields, fields: tab.fields,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
@@ -160,7 +160,7 @@ export const recurseNestedFields = ({
depth, depth,
fields: block.fields, fields: block.fields,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
@@ -176,7 +176,7 @@ export const recurseNestedFields = ({
depth, depth,
fields: field.fields, fields: field.fields,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
@@ -193,7 +193,7 @@ export const recurseNestedFields = ({
depth, depth,
field, field,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })

View File

@@ -5,7 +5,7 @@ import type { AdapterArguments } from '../types.js'
import { populate } from './populate.js' import { populate } from './populate.js'
import { recurseNestedFields } from './recurseNestedFields.js' import { recurseNestedFields } from './recurseNestedFields.js'
export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['populationPromise']>[0] export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['populationPromises']>[0]
type RecurseRichTextArgs = { type RecurseRichTextArgs = {
children: unknown[] children: unknown[]
@@ -13,7 +13,7 @@ type RecurseRichTextArgs = {
depth: number depth: number
field: RichTextField<any[], AdapterArguments, AdapterArguments> field: RichTextField<any[], AdapterArguments, AdapterArguments>
overrideAccess: boolean overrideAccess: boolean
promises: Promise<void>[] populationPromises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
} }
@@ -24,7 +24,7 @@ export const recurseRichText = ({
depth, depth,
field, field,
overrideAccess = false, overrideAccess = false,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}: RecurseRichTextArgs): void => { }: RecurseRichTextArgs): void => {
@@ -38,7 +38,7 @@ export const recurseRichText = ({
const collection = req.payload.collections[element?.relationTo] const collection = req.payload.collections[element?.relationTo]
if (collection) { if (collection) {
promises.push( populationPromises.push(
populate({ populate({
id: element.value.id, id: element.value.id,
collection, collection,
@@ -63,7 +63,7 @@ export const recurseRichText = ({
depth, depth,
fields: field.admin.upload.collections[element.relationTo].fields, fields: field.admin.upload.collections[element.relationTo].fields,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
@@ -75,7 +75,7 @@ export const recurseRichText = ({
const collection = req.payload.collections[element?.doc?.relationTo] const collection = req.payload.collections[element?.doc?.relationTo]
if (collection) { if (collection) {
promises.push( populationPromises.push(
populate({ populate({
id: element.doc.value, id: element.doc.value,
collection, collection,
@@ -99,7 +99,7 @@ export const recurseRichText = ({
depth, depth,
fields: field.admin?.link?.fields, fields: field.admin?.link?.fields,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
@@ -113,7 +113,7 @@ export const recurseRichText = ({
depth, depth,
field, field,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
@@ -122,27 +122,24 @@ export const recurseRichText = ({
} }
} }
export const richTextRelationshipPromise = async ({ export const richTextRelationshipPromise = ({
currentDepth, currentDepth,
depth, depth,
field, field,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}: Args): Promise<void> => { }: Args) => {
const promises = []
recurseRichText({ recurseRichText({
children: siblingDoc[field.name] as unknown[], children: siblingDoc[field.name] as unknown[],
currentDepth, currentDepth,
depth, depth,
field, field,
overrideAccess, overrideAccess,
promises, populationPromises,
req, req,
showHiddenFields, showHiddenFields,
}) })
await Promise.all(promises)
} }

View File

@@ -25,11 +25,12 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
}, },
} }
}, },
populationPromise({ populationPromises({
context, context,
currentDepth, currentDepth,
depth, depth,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
@@ -44,11 +45,12 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
field.admin?.elements?.includes('link') || field.admin?.elements?.includes('link') ||
!field?.admin?.elements !field?.admin?.elements
) { ) {
return richTextRelationshipPromise({ richTextRelationshipPromise({
context, context,
currentDepth, currentDepth,
depth, depth,
field, field,
fieldPromises,
findMany, findMany,
flattenLocales, flattenLocales,
overrideAccess, overrideAccess,
@@ -58,7 +60,6 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
siblingDoc, siblingDoc,
}) })
} }
return null
}, },
validate: richTextValidate, validate: richTextValidate,
} }

View File

@@ -19,6 +19,7 @@ type Args<T> = {
siblingData: Data siblingData: Data
} }
// TODO: Make this works for rich text subfields
export const defaultValuePromise = async <T>({ export const defaultValuePromise = async <T>({
id, id,
data, data,

View File

@@ -33,9 +33,15 @@ let serverURL: string
/** /**
* Client-side navigation to the lexical editor from list view * Client-side navigation to the lexical editor from list view
*/ */
async function navigateToLexicalFields(navigateToListView: boolean = true) { async function navigateToLexicalFields(
navigateToListView: boolean = true,
localized: boolean = false,
) {
if (navigateToListView) { if (navigateToListView) {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields') const url: AdminUrlUtil = new AdminUrlUtil(
serverURL,
localized ? 'lexical-localized-fields' : 'lexical-fields',
)
await page.goto(url.list) await page.goto(url.list)
} }
@@ -1101,7 +1107,7 @@ describe('lexical', () => {
) )
}) })
test.skip('should respect required error state in deeply nested text field', async () => { test('should respect required error state in deeply nested text field', async () => {
await navigateToLexicalFields() await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded() await richTextField.scrollIntoViewIfNeeded()
@@ -1117,12 +1123,18 @@ describe('lexical', () => {
await page.click('#action-save', { delay: 100 }) await page.click('#action-save', { delay: 100 })
await expect(page.locator('.Toastify')).toContainText('The following field is invalid') await expect(page.locator('.Toastify')).toContainText('The following field is invalid')
const requiredTooltip = conditionalArrayBlock
.locator('.tooltip-content:has-text("This field is required.")')
.first()
await requiredTooltip.scrollIntoViewIfNeeded()
// Check if error is shown next to field // Check if error is shown next to field
await expect( await expect(requiredTooltip).toBeInViewport() // toBeVisible() doesn't work for some reason
conditionalArrayBlock })
.locator('.tooltip-content:has-text("This field is required.")') })
.first(),
).toBeVisible() describe('localization', () => {
test.skip('ensure simple localized lexical field works', async () => {
await navigateToLexicalFields(true, true)
}) })
}) })
}) })

View File

@@ -27,7 +27,7 @@ export function generateLexicalRichText() {
{ {
format: '', format: '',
type: 'upload', type: 'upload',
version: 1, version: 2,
fields: { fields: {
caption: { caption: {
root: { root: {
@@ -57,11 +57,9 @@ export function generateLexicalRichText() {
{ {
format: '', format: '',
type: 'relationship', type: 'relationship',
version: 1, version: 2,
relationTo: 'text-fields', relationTo: 'text-fields',
value: { value: '{{TEXT_DOC_ID}}',
id: '{{TEXT_DOC_ID}}',
},
}, },
], ],
direction: 'ltr', direction: 'ltr',
@@ -69,9 +67,7 @@ export function generateLexicalRichText() {
}, },
}, },
relationTo: 'uploads', relationTo: 'uploads',
value: { value: '{{UPLOAD_DOC_ID}}',
id: '{{UPLOAD_DOC_ID}}',
},
}, },
{ {
format: '', format: '',
@@ -120,11 +116,9 @@ export function generateLexicalRichText() {
{ {
format: '', format: '',
type: 'relationship', type: 'relationship',
version: 1, version: 2,
relationTo: 'rich-text-fields', relationTo: 'rich-text-fields',
value: { value: '{{RICH_TEXT_DOC_ID}}',
id: '{{RICH_TEXT_DOC_ID}}',
},
}, },
{ {
children: [ children: [
@@ -173,11 +167,9 @@ export function generateLexicalRichText() {
{ {
format: '', format: '',
type: 'relationship', type: 'relationship',
version: 1, version: 2,
relationTo: 'text-fields', relationTo: 'text-fields',
value: { value: '{{TEXT_DOC_ID}}',
id: '{{TEXT_DOC_ID}}',
},
}, },
{ {
children: [ children: [

View File

@@ -0,0 +1,95 @@
import type { CollectionConfig } from 'payload/types'
import { BlocksFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
import { lexicalLocalizedFieldsSlug } from '../../slugs.js'
export const LexicalLocalizedFields: CollectionConfig = {
slug: lexicalLocalizedFieldsSlug,
admin: {
useAsTitle: 'title',
listSearchableFields: ['title'],
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
localized: true,
},
{
name: 'lexicalSimple',
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures],
}),
},
{
name: 'lexicalBlocksLocalized',
admin: {
description: 'Localized field with localized block subfields',
},
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
slug: 'block',
fields: [
{
name: 'textLocalized',
type: 'text',
localized: true,
},
{
name: 'rel',
type: 'relationship',
relationTo: lexicalLocalizedFieldsSlug,
},
],
},
],
}),
],
}),
},
{
name: 'lexicalBlocksSubLocalized',
type: 'richText',
admin: {
description: 'Non-localized field with localized block subfields',
},
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
slug: 'block',
fields: [
{
name: 'textLocalized',
type: 'text',
localized: true,
},
{
name: 'rel',
type: 'relationship',
relationTo: lexicalLocalizedFieldsSlug,
},
],
},
],
}),
],
}),
},
],
}

View File

@@ -0,0 +1,54 @@
import type { SerializedRelationshipNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState, SerializedParagraphNode, SerializedTextNode } from 'lexical'
import { lexicalLocalizedFieldsSlug } from '../../slugs.js'
export function textToLexicalJSON({
text,
lexicalLocalizedRelID,
}: {
lexicalLocalizedRelID?: number | string
text: string
}) {
const editorJSON: SerializedEditorState = {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
direction: 'ltr',
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text,
type: 'text',
version: 1,
} as SerializedTextNode,
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
} as SerializedParagraphNode,
],
},
}
if (lexicalLocalizedRelID) {
editorJSON.root.children.push({
format: '',
type: 'relationship',
version: 2,
relationTo: lexicalLocalizedFieldsSlug,
value: lexicalLocalizedRelID,
} as SerializedRelationshipNode)
}
return editorJSON
}

View File

@@ -116,10 +116,8 @@ export function generateLexicalRichText() {
{ {
format: '', format: '',
type: 'relationship', type: 'relationship',
version: 1, version: 2,
value: { value: '{{TEXT_DOC_ID}}',
id: '{{TEXT_DOC_ID}}',
},
relationTo: 'text-fields', relationTo: 'text-fields',
}, },
{ {
@@ -230,11 +228,9 @@ export function generateLexicalRichText() {
{ {
format: '', format: '',
type: 'upload', type: 'upload',
version: 1, version: 2,
relationTo: 'uploads', relationTo: 'uploads',
value: { value: '{{UPLOAD_DOC_ID}}',
id: '{{UPLOAD_DOC_ID}}',
},
fields: { fields: {
caption: { caption: {
root: { root: {

View File

@@ -12,6 +12,7 @@ import GroupFields from './collections/Group/index.js'
import IndexedFields from './collections/Indexed/index.js' import IndexedFields from './collections/Indexed/index.js'
import JSONFields from './collections/JSON/index.js' import JSONFields from './collections/JSON/index.js'
import { LexicalFields } from './collections/Lexical/index.js' import { LexicalFields } from './collections/Lexical/index.js'
import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js' import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
import NumberFields from './collections/Number/index.js' import NumberFields from './collections/Number/index.js'
import PointFields from './collections/Point/index.js' import PointFields from './collections/Point/index.js'
@@ -31,6 +32,7 @@ import { clearAndSeedEverything } from './seed.js'
export const collectionSlugs: CollectionConfig[] = [ export const collectionSlugs: CollectionConfig[] = [
LexicalFields, LexicalFields,
LexicalMigrateFields, LexicalMigrateFields,
LexicalLocalizedFields,
{ {
slug: 'users', slug: 'users',
admin: { admin: {

View File

@@ -416,9 +416,10 @@ describe('Lexical', () => {
/** /**
* Depth 1 population: * Depth 1 population:
*/ */
expect(subEditorRelationshipNode.value.id).toStrictEqual(createdRichTextDocID) expect(subEditorRelationshipNode.value).toStrictEqual(createdRichTextDocID)
// But the value should not be populated and only have the id field: // But the value should not be populated and only have the id field:
expect(Object.keys(subEditorRelationshipNode.value)).toHaveLength(1)
expect(typeof subEditorRelationshipNode.value).not.toStrictEqual('object')
}) })
it('should populate relationship nodes inside of a sub-editor from a blocks node with 1 depth', async () => { it('should populate relationship nodes inside of a sub-editor from a blocks node with 1 depth', async () => {
@@ -463,9 +464,9 @@ describe('Lexical', () => {
/** /**
* Depth 2 population: * Depth 2 population:
*/ */
expect(populatedDocEditorRelationshipNode.value.id).toStrictEqual(createdTextDocID) expect(populatedDocEditorRelationshipNode.value).toStrictEqual(createdTextDocID)
// But the value should not be populated and only have the id field - that's because it would require a depth of 2 // But the value should not be populated and only have the id field - that's because it would require a depth of 2
expect(Object.keys(populatedDocEditorRelationshipNode.value)).toHaveLength(1) expect(populatedDocEditorRelationshipNode.value).not.toStrictEqual('object')
}) })
it('should populate relationship nodes inside of a sub-editor from a blocks node with depth 2', async () => { it('should populate relationship nodes inside of a sub-editor from a blocks node with depth 2', async () => {

View File

@@ -15,6 +15,7 @@ import { dateDoc } from './collections/Date/shared.js'
import { groupDoc } from './collections/Group/shared.js' import { groupDoc } from './collections/Group/shared.js'
import { jsonDoc } from './collections/JSON/shared.js' import { jsonDoc } from './collections/JSON/shared.js'
import { lexicalDocData } from './collections/Lexical/data.js' import { lexicalDocData } from './collections/Lexical/data.js'
import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js'
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js' import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
import { numberDoc } from './collections/Number/shared.js' import { numberDoc } from './collections/Number/shared.js'
import { pointDoc } from './collections/Point/shared.js' import { pointDoc } from './collections/Point/shared.js'
@@ -35,6 +36,7 @@ import {
groupFieldsSlug, groupFieldsSlug,
jsonFieldsSlug, jsonFieldsSlug,
lexicalFieldsSlug, lexicalFieldsSlug,
lexicalLocalizedFieldsSlug,
lexicalMigrateFieldsSlug, lexicalMigrateFieldsSlug,
numberFieldsSlug, numberFieldsSlug,
pointFieldsSlug, pointFieldsSlug,
@@ -49,7 +51,7 @@ import {
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
export const seed = async (_payload) => { export const seed = async (_payload: Payload) => {
if (_payload.db.name === 'mongoose') { if (_payload.db.name === 'mongoose') {
await Promise.all( await Promise.all(
_payload.config.collections.map(async (coll) => { _payload.config.collections.map(async (coll) => {
@@ -274,6 +276,74 @@ export const seed = async (_payload) => {
overrideAccess: true, overrideAccess: true,
}) })
const lexicalLocalizedDoc1 = await _payload.create({
collection: lexicalLocalizedFieldsSlug,
data: {
title: 'Localized Lexical en',
lexicalSimple: textToLexicalJSON({ text: 'English text' }),
lexicalBlocksLocalized: textToLexicalJSON({ text: 'English text' }),
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'English text' }),
},
locale: 'en',
depth: 0,
overrideAccess: true,
})
await _payload.update({
collection: lexicalLocalizedFieldsSlug,
id: lexicalLocalizedDoc1.id,
data: {
title: 'Localized Lexical es',
lexicalSimple: textToLexicalJSON({ text: 'Spanish text' }),
lexicalBlocksLocalized: textToLexicalJSON({ text: 'Spanish text' }),
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'Spanish text' }),
},
locale: 'es',
depth: 0,
overrideAccess: true,
})
const lexicalLocalizedDoc2 = await _payload.create({
collection: lexicalLocalizedFieldsSlug,
data: {
title: 'Localized Lexical en 2',
lexicalSimple: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksLocalized: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksSubLocalized: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
},
locale: 'en',
depth: 0,
overrideAccess: true,
})
await _payload.update({
collection: lexicalLocalizedFieldsSlug,
id: lexicalLocalizedDoc2.id,
data: {
title: 'Localized Lexical es 2',
lexicalSimple: textToLexicalJSON({
text: 'Spanish text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksLocalized: textToLexicalJSON({
text: 'Spanish text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
},
locale: 'es',
depth: 0,
overrideAccess: true,
})
await _payload.create({ await _payload.create({
collection: lexicalMigrateFieldsSlug, collection: lexicalMigrateFieldsSlug,
data: lexicalMigrateDocWithRelId, data: lexicalMigrateDocWithRelId,

View File

@@ -1,28 +1,29 @@
export const usersSlug = 'users' as const export const usersSlug = 'users'
export const arrayFieldsSlug = 'array-fields' as const export const arrayFieldsSlug = 'array-fields'
export const blockFieldsSlug = 'block-fields' as const export const blockFieldsSlug = 'block-fields'
export const checkboxFieldsSlug = 'checkbox-fields' as const export const checkboxFieldsSlug = 'checkbox-fields'
export const codeFieldsSlug = 'code-fields' as const export const codeFieldsSlug = 'code-fields'
export const collapsibleFieldsSlug = 'collapsible-fields' as const export const collapsibleFieldsSlug = 'collapsible-fields'
export const conditionalLogicSlug = 'conditional-logic' as const export const conditionalLogicSlug = 'conditional-logic'
export const dateFieldsSlug = 'date-fields' as const export const dateFieldsSlug = 'date-fields'
export const groupFieldsSlug = 'group-fields' as const export const groupFieldsSlug = 'group-fields'
export const indexedFieldsSlug = 'indexed-fields' as const export const indexedFieldsSlug = 'indexed-fields'
export const jsonFieldsSlug = 'json-fields' as const export const jsonFieldsSlug = 'json-fields'
export const lexicalFieldsSlug = 'lexical-fields' as const export const lexicalFieldsSlug = 'lexical-fields'
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields' as const export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields'
export const numberFieldsSlug = 'number-fields' as const export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
export const pointFieldsSlug = 'point-fields' as const export const numberFieldsSlug = 'number-fields'
export const radioFieldsSlug = 'radio-fields' as const export const pointFieldsSlug = 'point-fields'
export const relationshipFieldsSlug = 'relationship-fields' as const export const radioFieldsSlug = 'radio-fields'
export const richTextFieldsSlug = 'rich-text-fields' as const export const relationshipFieldsSlug = 'relationship-fields'
export const rowFieldsSlug = 'row-fields' as const export const richTextFieldsSlug = 'rich-text-fields'
export const selectFieldsSlug = 'select-fields' as const export const rowFieldsSlug = 'row-fields'
export const tabsFieldsSlug = 'tabs-fields' as const export const selectFieldsSlug = 'select-fields'
export const textFieldsSlug = 'text-fields' as const export const tabsFieldsSlug = 'tabs-fields'
export const uploadsSlug = 'uploads' as const export const textFieldsSlug = 'text-fields'
export const uploads2Slug = 'uploads2' as const export const uploadsSlug = 'uploads'
export const uploads3Slug = 'uploads3' as const export const uploads2Slug = 'uploads2'
export const uploads3Slug = 'uploads3'
export const collectionSlugs = [ export const collectionSlugs = [
usersSlug, usersSlug,
arrayFieldsSlug, arrayFieldsSlug,