= (props) => {
ref={inlineBlockElemElemRef}
>
{LabelComponent ? (
-
+
) : (
- {getTranslation(reducedBlock.labels.singular, i18n)}
+ {getTranslation(reducedBlock.labels?.singular, i18n)}
)}
{editor.isEditable() && (
diff --git a/packages/richtext-lexical/src/features/blocks/client/index.tsx b/packages/richtext-lexical/src/features/blocks/client/index.tsx
new file mode 100644
index 00000000000..495740bd176
--- /dev/null
+++ b/packages/richtext-lexical/src/features/blocks/client/index.tsx
@@ -0,0 +1,196 @@
+'use client'
+
+import type { BlocksFieldProps } from 'payload'
+
+import { getTranslation } from '@payloadcms/translations'
+
+import type { SlashMenuItem } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.js'
+import type { ToolbarGroup, ToolbarGroupItem } from '../../toolbars/types.js'
+
+import { BlockIcon } from '../../../lexical/ui/icons/Block/index.js'
+import { InlineBlocksIcon } from '../../../lexical/ui/icons/InlineBlocks/index.js'
+import { createClientFeature } from '../../../utilities/createClientFeature.js'
+import { BlockNode } from './nodes/BlocksNode.js'
+import { InlineBlockNode } from './nodes/InlineBlocksNode.js'
+import { INSERT_BLOCK_COMMAND, OPEN_INLINE_BLOCK_DRAWER_COMMAND } from './plugin/commands.js'
+import { BlocksPlugin } from './plugin/index.js'
+
+export type BlocksFeatureClientProps = {
+ reducedBlockSlugs: string[]
+ reducedInlineBlockSlugs: string[]
+}
+
+export const BlocksFeatureClient = createClientFeature(({ props }) => ({
+ nodes: [BlockNode, InlineBlockNode],
+ plugins: [
+ {
+ Component: BlocksPlugin,
+ position: 'normal',
+ },
+ ],
+ sanitizedClientFeatureProps: props,
+ slashMenu: {
+ groups: [
+ props.reducedBlockSlugs?.length
+ ? {
+ items: props.reducedBlockSlugs.map((blockSlug) => {
+ return {
+ Icon: BlockIcon,
+ key: 'block-' + blockSlug,
+ keywords: ['block', 'blocks', blockSlug],
+ label: ({ i18n, richTextComponentMap }) => {
+ const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_blocks`
+ const mappedBlock = richTextComponentMap.get(componentMapRenderedBlockPath)[0]
+
+ const blockFieldComponentProps: BlocksFieldProps = mappedBlock.fieldComponentProps
+
+ const reducedBlock = blockFieldComponentProps.blocks.find(
+ (_block) => _block.slug === blockSlug,
+ )
+
+ const blockDisplayName = reducedBlock.labels.singular
+ ? getTranslation(reducedBlock.labels.singular, i18n)
+ : reducedBlock.slug
+
+ return blockDisplayName
+ },
+ onSelect: ({ editor }) => {
+ editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
+ id: null,
+ blockName: '',
+ blockType: blockSlug,
+ })
+ },
+ } as SlashMenuItem
+ }),
+ key: 'blocks',
+ label: ({ i18n }) => {
+ return i18n.t('lexical:blocks:label')
+ },
+ }
+ : null,
+ props.reducedInlineBlockSlugs?.length
+ ? {
+ items: props.reducedInlineBlockSlugs.map((inlineBlockSlug) => {
+ return {
+ Icon: InlineBlocksIcon,
+ key: 'inlineBlocks-' + inlineBlockSlug,
+ keywords: ['inlineBlock', 'inline block', inlineBlockSlug],
+ label: ({ i18n, richTextComponentMap }) => {
+ const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_inline_blocks`
+ const mappedBlock = richTextComponentMap.get(componentMapRenderedBlockPath)[0]
+
+ const blockFieldComponentProps: BlocksFieldProps = mappedBlock.fieldComponentProps
+
+ const reducedBlock = blockFieldComponentProps.blocks.find(
+ (_block) => _block.slug === inlineBlockSlug,
+ )
+
+ const blockDisplayName = reducedBlock.labels.singular
+ ? getTranslation(reducedBlock.labels.singular, i18n)
+ : reducedBlock.slug
+
+ return blockDisplayName
+ },
+ onSelect: ({ editor }) => {
+ editor.dispatchCommand(OPEN_INLINE_BLOCK_DRAWER_COMMAND, {
+ fields: {
+ id: null,
+ blockName: '',
+ blockType: inlineBlockSlug,
+ },
+ })
+ },
+ } as SlashMenuItem
+ }),
+ key: 'inlineBlocks',
+ label: ({ i18n }) => {
+ return i18n.t('lexical:blocks:inlineBlocks:label')
+ },
+ }
+ : null,
+ ].filter(Boolean),
+ },
+ toolbarFixed: {
+ groups: [
+ props.reducedBlockSlugs?.length
+ ? {
+ type: 'dropdown',
+ ChildComponent: BlockIcon,
+ items: props.reducedBlockSlugs.map((blockSlug, index) => {
+ return {
+ ChildComponent: BlockIcon,
+ isActive: undefined, // At this point, we would be inside a sub-richtext-editor. And at this point this will be run against the focused sub-editor, not the parent editor which has the actual block. Thus, no point in running this
+ key: 'block-' + blockSlug,
+ label: ({ i18n, richTextComponentMap }) => {
+ const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_blocks`
+ const mappedBlock = richTextComponentMap.get(componentMapRenderedBlockPath)[0]
+
+ const blockFieldComponentProps: BlocksFieldProps = mappedBlock.fieldComponentProps
+
+ const reducedBlock = blockFieldComponentProps.blocks.find(
+ (_block) => _block.slug === blockSlug,
+ )
+
+ const blockDisplayName = reducedBlock.labels.singular
+ ? getTranslation(reducedBlock.labels.singular, i18n)
+ : reducedBlock.slug
+
+ return blockDisplayName
+ },
+ onSelect: ({ editor }) => {
+ editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
+ id: null,
+ blockName: '',
+ blockType: blockSlug,
+ })
+ },
+ order: index,
+ } as ToolbarGroupItem
+ }),
+ key: 'blocks',
+ order: 20,
+ }
+ : null,
+ props.reducedInlineBlockSlugs?.length
+ ? {
+ type: 'dropdown',
+ ChildComponent: InlineBlocksIcon,
+ items: props.reducedInlineBlockSlugs.map((inlineBlockSlug, index) => {
+ return {
+ ChildComponent: InlineBlocksIcon,
+ isActive: undefined,
+ key: 'inlineBlock-' + inlineBlockSlug,
+ label: ({ i18n, richTextComponentMap }) => {
+ const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_inline_blocks`
+ const mappedBlock = richTextComponentMap.get(componentMapRenderedBlockPath)[0]
+
+ const blockFieldComponentProps: BlocksFieldProps = mappedBlock.fieldComponentProps
+
+ const reducedBlock = blockFieldComponentProps.blocks.find(
+ (_block) => _block.slug === inlineBlockSlug,
+ )
+
+ const blockDisplayName = reducedBlock.labels.singular
+ ? getTranslation(reducedBlock.labels.singular, i18n)
+ : reducedBlock.slug
+
+ return blockDisplayName
+ },
+ onSelect: ({ editor }) => {
+ editor.dispatchCommand(OPEN_INLINE_BLOCK_DRAWER_COMMAND, {
+ fields: {
+ blockType: inlineBlockSlug,
+ },
+ })
+ },
+ order: index,
+ } as ToolbarGroupItem
+ }),
+ key: 'inlineBlocks',
+ order: 25,
+ }
+ : null,
+ ].filter(Boolean) as ToolbarGroup[],
+ },
+}))
diff --git a/packages/richtext-lexical/src/features/blocks/client/nodes/BlocksNode.tsx b/packages/richtext-lexical/src/features/blocks/client/nodes/BlocksNode.tsx
new file mode 100644
index 00000000000..9a6c45fcaed
--- /dev/null
+++ b/packages/richtext-lexical/src/features/blocks/client/nodes/BlocksNode.tsx
@@ -0,0 +1,49 @@
+import type { EditorConfig, LexicalEditor, LexicalNode } from 'lexical'
+
+import ObjectID from 'bson-objectid'
+import React, { type JSX } from 'react'
+
+import type { BlockFields, SerializedBlockNode } from '../../server/nodes/BlocksNode.js'
+
+import { ServerBlockNode } from '../../server/nodes/BlocksNode.js'
+
+const BlockComponent = React.lazy(() =>
+ import('../component/index.js').then((module) => ({
+ default: module.BlockComponent,
+ })),
+)
+
+export class BlockNode extends ServerBlockNode {
+ static importJSON(serializedNode: SerializedBlockNode): BlockNode {
+ if (serializedNode.version === 1) {
+ // Convert (version 1 had the fields wrapped in another, unnecessary data property)
+ serializedNode = {
+ ...serializedNode,
+ fields: {
+ ...(serializedNode as any).fields.data,
+ },
+ version: 2,
+ }
+ }
+ const node = $createBlockNode(serializedNode.fields)
+ node.setFormat(serializedNode.format)
+ return node
+ }
+
+ decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
+ return
+ }
+}
+
+export function $createBlockNode(fields: Exclude): BlockNode {
+ return new BlockNode({
+ fields: {
+ ...fields,
+ id: fields?.id || new ObjectID.default().toHexString(),
+ },
+ })
+}
+
+export function $isBlockNode(node: BlockNode | LexicalNode | null | undefined): node is BlockNode {
+ return node instanceof BlockNode
+}
diff --git a/packages/richtext-lexical/src/features/blocks/client/nodes/InlineBlocksNode.tsx b/packages/richtext-lexical/src/features/blocks/client/nodes/InlineBlocksNode.tsx
new file mode 100644
index 00000000000..76e80b38089
--- /dev/null
+++ b/packages/richtext-lexical/src/features/blocks/client/nodes/InlineBlocksNode.tsx
@@ -0,0 +1,61 @@
+import type {
+ EditorConfig,
+ LexicalEditor,
+ LexicalNode,
+ SerializedLexicalNode,
+ Spread,
+} from 'lexical'
+
+import ObjectID from 'bson-objectid'
+import React, { type JSX } from 'react'
+
+import { ServerInlineBlockNode } from '../../server/nodes/InlineBlocksNode.js'
+
+export type InlineBlockFields = {
+ /** Block form data */
+ [key: string]: any
+ //blockName: string
+ blockType: string
+ id: string
+}
+
+const InlineBlockComponent = React.lazy(() =>
+ import('../componentInline/index.js').then((module) => ({
+ default: module.InlineBlockComponent,
+ })),
+)
+
+export type SerializedInlineBlockNode = Spread<
+ {
+ children?: never // required so that our typed editor state doesn't automatically add children
+ fields: InlineBlockFields
+ type: 'inlineBlock'
+ },
+ SerializedLexicalNode
+>
+
+export class InlineBlockNode extends ServerInlineBlockNode {
+ static importJSON(serializedNode: SerializedInlineBlockNode): InlineBlockNode {
+ const node = $createInlineBlockNode(serializedNode.fields)
+ return node
+ }
+
+ decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
+ return
+ }
+}
+
+export function $createInlineBlockNode(fields: Exclude): InlineBlockNode {
+ return new InlineBlockNode({
+ fields: {
+ ...fields,
+ id: fields?.id || new ObjectID.default().toHexString(),
+ },
+ })
+}
+
+export function $isInlineBlockNode(
+ node: InlineBlockNode | LexicalNode | null | undefined,
+): node is InlineBlockNode {
+ return node instanceof InlineBlockNode
+}
diff --git a/packages/richtext-lexical/src/features/blocks/plugin/commands.ts b/packages/richtext-lexical/src/features/blocks/client/plugin/commands.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/blocks/plugin/commands.ts
rename to packages/richtext-lexical/src/features/blocks/client/plugin/commands.ts
diff --git a/packages/richtext-lexical/src/features/blocks/plugin/index.tsx b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx
similarity index 79%
rename from packages/richtext-lexical/src/features/blocks/plugin/index.tsx
rename to packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx
index f5b4dd237cf..eb71ed401ee 100644
--- a/packages/richtext-lexical/src/features/blocks/plugin/index.tsx
+++ b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx
@@ -1,8 +1,10 @@
'use client'
+import type { BlocksFieldProps } from 'payload'
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
import { $insertNodeToNearestRoot, $wrapNodeInElement, mergeRegister } from '@lexical/utils'
import { getTranslation } from '@payloadcms/translations'
-import { useModal, useTranslation } from '@payloadcms/ui'
+import { useFieldProps, useModal, useTranslation } from '@payloadcms/ui'
import {
$createParagraphNode,
$getNodeByKey,
@@ -17,13 +19,13 @@ import {
} from 'lexical'
import React, { useEffect, useState } from 'react'
-import type { ClientComponentProps, PluginComponent } from '../../typesClient.js'
-import type { BlocksFeatureClientProps, ClientBlock } from '../feature.client.js'
-import type { BlockFields } from '../nodes/BlocksNode.js'
+import type { PluginComponent } from '../../../typesClient.js'
+import type { BlockFields } from '../../server/nodes/BlocksNode.js'
+import type { BlocksFeatureClientProps } from '../index.js'
import type { InlineBlockNode } from '../nodes/InlineBlocksNode.js'
-import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
-import { FieldsDrawer } from '../../../utilities/fieldsDrawer/Drawer.js'
+import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
+import { FieldsDrawer } from '../../../../utilities/fieldsDrawer/Drawer.js'
import { $createBlockNode, BlockNode } from '../nodes/BlocksNode.js'
import { $createInlineBlockNode } from '../nodes/InlineBlocksNode.js'
import {
@@ -43,13 +45,24 @@ export const BlocksPlugin: PluginComponent = () => {
const [blockType, setBlockType] = useState('' as any)
const [targetNodeKey, setTargetNodeKey] = useState(null)
const { i18n, t } = useTranslation()
+ const { schemaPath } = useFieldProps()
+
+ const {
+ field: { richTextComponentMap },
+ } = useEditorConfigContext()
+
+ const schemaFieldsPath = `${schemaPath}.lexical_internal_feature.blocks.lexical_inline_blocks.lexical_inline_blocks.${blockFields?.blockType}`
- const { editorConfig } = useEditorConfigContext()
+ const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_inline_blocks`
+ const mappedBlock = richTextComponentMap.has(componentMapRenderedBlockPath)
+ ? richTextComponentMap.get(componentMapRenderedBlockPath)[0]
+ : null
- const reducedBlock: ClientBlock = (
- editorConfig?.resolvedFeatureMap?.get('blocks')
- ?.sanitizedClientFeatureProps as ClientComponentProps
- )?.reducedInlineBlocks?.find((block) => block.slug === blockFields?.blockType)
+ const blockFieldComponentProps: BlocksFieldProps = mappedBlock?.fieldComponentProps
+
+ const reducedBlock = blockFieldComponentProps
+ ? blockFieldComponentProps.blocks.find((block) => block.slug === blockFields?.blockType)
+ : null
useEffect(() => {
if (!editor.hasNodes([BlockNode])) {
@@ -148,6 +161,10 @@ export const BlocksPlugin: PluginComponent = () => {
)
}, [editor, targetNodeKey, toggleModal])
+ if (!mappedBlock) {
+ return null
+ }
+
const blockDisplayName = reducedBlock?.labels?.singular
? getTranslation(reducedBlock?.labels?.singular, i18n)
: reducedBlock?.slug
@@ -160,6 +177,7 @@ export const BlocksPlugin: PluginComponent = () => {
label: blockDisplayName ?? t('lexical:blocks:inlineBlocks:label'),
})}
featureKey="blocks"
+ fieldMapOverride={reducedBlock?.fieldMap}
handleDrawerSubmit={(_fields, data) => {
closeModal(drawerSlug)
if (!data) {
@@ -170,6 +188,7 @@ export const BlocksPlugin: PluginComponent = () => {
editor.dispatchCommand(INSERT_INLINE_BLOCK_COMMAND, data)
}}
+ schemaFieldsPathOverride={schemaFieldsPath}
schemaPathSuffix={blockFields?.blockType}
/>
)
diff --git a/packages/richtext-lexical/src/features/blocks/feature.client.tsx b/packages/richtext-lexical/src/features/blocks/feature.client.tsx
deleted file mode 100644
index adf384b958a..00000000000
--- a/packages/richtext-lexical/src/features/blocks/feature.client.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-'use client'
-
-import type { Block, ReducedBlock } from 'payload'
-
-import { getTranslation } from '@payloadcms/translations'
-
-import type { ToolbarGroup } from '../toolbars/types.js'
-
-import { BlockIcon } from '../../lexical/ui/icons/Block/index.js'
-import { InlineBlocksIcon } from '../../lexical/ui/icons/InlineBlocks/index.js'
-import { createClientFeature } from '../../utilities/createClientFeature.js'
-import { BlockNode } from './nodes/BlocksNode.js'
-import { InlineBlockNode } from './nodes/InlineBlocksNode.js'
-import { INSERT_BLOCK_COMMAND, OPEN_INLINE_BLOCK_DRAWER_COMMAND } from './plugin/commands.js'
-import { BlocksPlugin } from './plugin/index.js'
-
-export type ClientBlock = {
- LabelComponent?: Block['admin']['components']['Label']
-} & ReducedBlock
-
-export type BlocksFeatureClientProps = {
- reducedBlocks: ClientBlock[]
- reducedInlineBlocks: ClientBlock[]
-}
-
-export const BlocksFeatureClient = createClientFeature(({ props }) => ({
- nodes: [BlockNode, InlineBlockNode],
- plugins: [
- {
- Component: BlocksPlugin,
- position: 'normal',
- },
- ],
- sanitizedClientFeatureProps: props,
- slashMenu: {
- groups: [
- props.reducedBlocks?.length
- ? {
- items: props.reducedBlocks.map((block) => {
- return {
- Icon: BlockIcon,
- key: 'block-' + block.slug,
- keywords: ['block', 'blocks', block.slug],
- label: ({ i18n }) => {
- if (!block.labels.singular) {
- return block.slug
- }
-
- return getTranslation(block.labels.singular, i18n)
- },
- onSelect: ({ editor }) => {
- editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
- id: null,
- blockName: '',
- blockType: block.slug,
- })
- },
- }
- }),
- key: 'blocks',
- label: ({ i18n }) => {
- return i18n.t('lexical:blocks:label')
- },
- }
- : null,
- props.reducedInlineBlocks?.length
- ? {
- items: props.reducedInlineBlocks.map((inlineBlock) => {
- return {
- Icon: InlineBlocksIcon,
- key: 'inlineBlocks-' + inlineBlock.slug,
- keywords: ['inlineBlock', 'inline block', inlineBlock.slug],
- label: ({ i18n }) => {
- if (!inlineBlock.labels.singular) {
- return inlineBlock.slug
- }
-
- return getTranslation(inlineBlock.labels.singular, i18n)
- },
- onSelect: ({ editor }) => {
- editor.dispatchCommand(OPEN_INLINE_BLOCK_DRAWER_COMMAND, {
- fields: {
- id: null,
- blockName: '',
- blockType: inlineBlock.slug,
- },
- })
- },
- }
- }),
- key: 'inlineBlocks',
- label: ({ i18n }) => {
- return i18n.t('lexical:blocks:inlineBlocks:label')
- },
- }
- : null,
- ].filter(Boolean),
- },
- toolbarFixed: {
- groups: [
- props.reducedBlocks?.length
- ? {
- type: 'dropdown',
- ChildComponent: BlockIcon,
- items: props.reducedBlocks.map((block, index) => {
- return {
- ChildComponent: BlockIcon,
- isActive: undefined, // At this point, we would be inside a sub-richtext-editor. And at this point this will be run against the focused sub-editor, not the parent editor which has the actual block. Thus, no point in running this
- key: 'block-' + block.slug,
- label: ({ i18n }) => {
- if (!block.labels.singular) {
- return block.slug
- }
-
- return getTranslation(block.labels.singular, i18n)
- },
- onSelect: ({ editor }) => {
- editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
- id: null,
- blockName: '',
- blockType: block.slug,
- })
- },
- order: index,
- }
- }),
- key: 'blocks',
- order: 20,
- }
- : null,
- props.reducedInlineBlocks?.length
- ? {
- type: 'dropdown',
- ChildComponent: InlineBlocksIcon,
- items: props.reducedInlineBlocks.map((inlineBlock, index) => {
- return {
- ChildComponent: InlineBlocksIcon,
- isActive: undefined,
- key: 'inlineBlock-' + inlineBlock.slug,
- label: ({ i18n }) => {
- if (!inlineBlock.labels.singular) {
- return inlineBlock.slug
- }
-
- return getTranslation(inlineBlock.labels.singular, i18n)
- },
- onSelect: ({ editor }) => {
- editor.dispatchCommand(OPEN_INLINE_BLOCK_DRAWER_COMMAND, {
- fields: {
- blockType: inlineBlock.slug,
- },
- })
- },
- order: index,
- }
- }),
- key: 'inlineBlocks',
- order: 25,
- }
- : null,
- ].filter(Boolean) as ToolbarGroup[],
- },
-}))
diff --git a/packages/richtext-lexical/src/features/blocks/graphQLPopulationPromise.ts b/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts
similarity index 81%
rename from packages/richtext-lexical/src/features/blocks/graphQLPopulationPromise.ts
rename to packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts
index 410247299d5..158fc0efdad 100644
--- a/packages/richtext-lexical/src/features/blocks/graphQLPopulationPromise.ts
+++ b/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts
@@ -1,10 +1,10 @@
import type { Block } from 'payload'
-import type { PopulationPromise } from '../typesServer.js'
+import type { PopulationPromise } from '../../typesServer.js'
+import type { SerializedInlineBlockNode } from '../client/nodes/InlineBlocksNode.js'
import type { SerializedBlockNode } from './nodes/BlocksNode.js'
-import type { SerializedInlineBlockNode } from './nodes/InlineBlocksNode.js'
-import { recursivelyPopulateFieldsForGraphQL } from '../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
+import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
export const blockPopulationPromiseHOC = (
blocks: Block[],
diff --git a/packages/richtext-lexical/src/features/blocks/i18n.ts b/packages/richtext-lexical/src/features/blocks/server/i18n.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/blocks/i18n.ts
rename to packages/richtext-lexical/src/features/blocks/server/i18n.ts
diff --git a/packages/richtext-lexical/src/features/blocks/feature.server.ts b/packages/richtext-lexical/src/features/blocks/server/index.ts
similarity index 70%
rename from packages/richtext-lexical/src/features/blocks/feature.server.ts
rename to packages/richtext-lexical/src/features/blocks/server/index.ts
index 5bf34528d4b..7140b2ce8fa 100644
--- a/packages/richtext-lexical/src/features/blocks/feature.server.ts
+++ b/packages/richtext-lexical/src/features/blocks/server/index.ts
@@ -2,16 +2,14 @@ import type { Block, BlockField, Config, Field } from 'payload'
import { baseBlockFields, fieldsToJSONSchema, formatLabels, sanitizeFields } from 'payload'
-import type { BlocksFeatureClientProps } from './feature.client.js'
+import type { BlocksFeatureClientProps } from '../client/index.js'
-// eslint-disable-next-line payload/no-imports-from-exports-dir
-import { BlocksFeatureClient } from '../../exports/client/index.js'
-import { createServerFeature } from '../../utilities/createServerFeature.js'
-import { createNode } from '../typeUtilities.js'
+import { createServerFeature } from '../../../utilities/createServerFeature.js'
+import { createNode } from '../../typeUtilities.js'
import { blockPopulationPromiseHOC } from './graphQLPopulationPromise.js'
import { i18n } from './i18n.js'
-import { BlockNode } from './nodes/BlocksNode.js'
-import { InlineBlockNode } from './nodes/InlineBlocksNode.js'
+import { ServerBlockNode } from './nodes/BlocksNode.js'
+import { ServerInlineBlockNode } from './nodes/InlineBlocksNode.js'
import { blockValidationHOC } from './validate.js'
export type BlocksFeatureProps = {
@@ -25,6 +23,12 @@ export const BlocksFeature = createServerFeature<
BlocksFeatureClientProps
>({
feature: async ({ config: _config, isRoot, props }) => {
+ // Build clientProps
+ const clientProps: BlocksFeatureClientProps = {
+ reducedBlockSlugs: [],
+ reducedInlineBlockSlugs: [],
+ }
+
if (props?.blocks?.length || props?.inlineBlocks?.length) {
const validRelationships = _config.collections.map((c) => c.slug) || []
@@ -38,7 +42,9 @@ export const BlocksFeature = createServerFeature<
fields: block.fields,
requireFieldLevelRichTextEditor: isRoot,
validRelationships,
- })
+ }) // TODO: Do I need to sanitize fields?
+
+ clientProps.reducedBlockSlugs.push(block.slug)
}
}
if (props?.inlineBlocks?.length) {
@@ -51,43 +57,15 @@ export const BlocksFeature = createServerFeature<
fields: block.fields,
requireFieldLevelRichTextEditor: isRoot,
validRelationships,
- })
- }
- }
- }
+ }) // TODO: Do I need to sanitize fields?
- // Build clientProps
- const clientProps: BlocksFeatureClientProps = {
- reducedBlocks: [],
- reducedInlineBlocks: [],
- }
- if (props?.blocks?.length) {
- for (const block of props.blocks) {
- clientProps.reducedBlocks.push({
- slug: block.slug,
- LabelComponent: block?.admin?.components?.Label,
- fieldMap: [],
- imageAltText: block.imageAltText,
- imageURL: block.imageURL,
- labels: block.labels,
- })
- }
- }
- if (props?.inlineBlocks?.length) {
- for (const block of props.inlineBlocks) {
- clientProps.reducedInlineBlocks.push({
- slug: block.slug,
- LabelComponent: block?.admin?.components?.Label,
- fieldMap: [],
- imageAltText: block.imageAltText,
- imageURL: block.imageURL,
- labels: block.labels,
- })
+ clientProps.reducedInlineBlockSlugs.push(block.slug)
+ }
}
}
return {
- ClientFeature: BlocksFeatureClient,
+ ClientFeature: '@payloadcms/richtext-lexical/client#BlocksFeatureClient',
clientFeatureProps: clientProps,
generateSchemaMap: ({ props }) => {
/**
@@ -97,15 +75,24 @@ export const BlocksFeature = createServerFeature<
const schemaMap = new Map()
if (props?.blocks?.length) {
- for (const block of props.blocks) {
- schemaMap.set(block.slug, block.fields || [])
- }
+ schemaMap.set('lexical_blocks', [
+ {
+ name: 'lexical_blocks',
+ type: 'blocks',
+ blocks: props.blocks,
+ },
+ ])
}
if (props?.inlineBlocks?.length) {
- for (const block of props.inlineBlocks) {
- schemaMap.set(block.slug, block.fields || [])
- }
+ // To generate block schemaMap which generates things like the componentMap for admin.Label
+ schemaMap.set('lexical_inline_blocks', [
+ {
+ name: 'lexical_inline_blocks',
+ type: 'blocks',
+ blocks: props.inlineBlocks,
+ },
+ ])
}
return schemaMap
@@ -152,6 +139,19 @@ export const BlocksFeature = createServerFeature<
nodes: [
createNode({
getSubFields: ({ node }) => {
+ if (!node) {
+ if (props?.blocks?.length) {
+ return [
+ {
+ name: 'lexical_blocks',
+ type: 'blocks',
+ blocks: props.blocks,
+ },
+ ]
+ }
+ return []
+ }
+
const blockType = node.fields.blockType
const block = props.blocks.find((block) => block.slug === blockType)
@@ -161,11 +161,24 @@ export const BlocksFeature = createServerFeature<
return node?.fields
},
graphQLPopulationPromises: [blockPopulationPromiseHOC(props.blocks)],
- node: BlockNode,
+ node: ServerBlockNode,
validations: [blockValidationHOC(props.blocks)],
}),
createNode({
getSubFields: ({ node }) => {
+ if (!node) {
+ if (props?.inlineBlocks?.length) {
+ return [
+ {
+ name: 'lexical_inline_blocks',
+ type: 'blocks',
+ blocks: props.inlineBlocks,
+ },
+ ]
+ }
+ return []
+ }
+
const blockType = node.fields.blockType
const block = props.inlineBlocks.find((block) => block.slug === blockType)
@@ -175,7 +188,7 @@ export const BlocksFeature = createServerFeature<
return node?.fields
},
graphQLPopulationPromises: [blockPopulationPromiseHOC(props.inlineBlocks)],
- node: InlineBlockNode,
+ node: ServerInlineBlockNode,
validations: [blockValidationHOC(props.inlineBlocks)],
}),
],
diff --git a/packages/richtext-lexical/src/features/blocks/nodes/BlocksNode.tsx b/packages/richtext-lexical/src/features/blocks/server/nodes/BlocksNode.tsx
similarity index 74%
rename from packages/richtext-lexical/src/features/blocks/nodes/BlocksNode.tsx
rename to packages/richtext-lexical/src/features/blocks/server/nodes/BlocksNode.tsx
index e2407cb4312..c71e14f4351 100644
--- a/packages/richtext-lexical/src/features/blocks/nodes/BlocksNode.tsx
+++ b/packages/richtext-lexical/src/features/blocks/server/nodes/BlocksNode.tsx
@@ -2,9 +2,7 @@ import type { SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecorat
import type {
DOMConversionMap,
DOMExportOutput,
- EditorConfig,
ElementFormatType,
- LexicalEditor,
LexicalNode,
NodeKey,
Spread,
@@ -14,7 +12,6 @@ import type { JsonObject } from 'payload'
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
import ObjectID from 'bson-objectid'
import { deepCopyObjectSimple } from 'payload/shared'
-import React, { type JSX } from 'react'
export type BlockFields = {
/** Block form data */
@@ -23,12 +20,6 @@ export type BlockFields = {
id: string
} & TBlockFields
-const BlockComponent = React.lazy(() =>
- import('../component/index.js').then((module) => ({
- default: module.BlockComponent,
- })),
-)
-
export type SerializedBlockNode = Spread<
{
children?: never // required so that our typed editor state doesn't automatically add children
@@ -38,7 +29,7 @@ export type SerializedBlockNode =
SerializedDecoratorBlockNode
>
-export class BlockNode extends DecoratorBlockNode {
+export class ServerBlockNode extends DecoratorBlockNode {
__fields: BlockFields
constructor({
@@ -54,8 +45,8 @@ export class BlockNode extends DecoratorBlockNode {
this.__fields = fields
}
- static clone(node: BlockNode): BlockNode {
- return new BlockNode({
+ static clone(node: ServerBlockNode): ServerBlockNode {
+ return new this({
fields: node.__fields,
format: node.__format,
key: node.__key,
@@ -70,7 +61,7 @@ export class BlockNode extends DecoratorBlockNode {
return {}
}
- static importJSON(serializedNode: SerializedBlockNode): BlockNode {
+ static importJSON(serializedNode: SerializedBlockNode): ServerBlockNode {
if (serializedNode.version === 1) {
// Convert (version 1 had the fields wrapped in another, unnecessary data property)
serializedNode = {
@@ -81,7 +72,7 @@ export class BlockNode extends DecoratorBlockNode {
version: 2,
}
}
- const node = $createBlockNode(serializedNode.fields)
+ const node = $createServerBlockNode(serializedNode.fields)
node.setFormat(serializedNode.format)
return node
}
@@ -89,9 +80,6 @@ export class BlockNode extends DecoratorBlockNode {
static isInline(): false {
return false
}
- decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
- return
- }
exportDOM(): DOMExportOutput {
const element = document.createElement('div')
@@ -126,8 +114,8 @@ export class BlockNode extends DecoratorBlockNode {
}
}
-export function $createBlockNode(fields: Exclude): BlockNode {
- return new BlockNode({
+export function $createServerBlockNode(fields: Exclude): ServerBlockNode {
+ return new ServerBlockNode({
fields: {
...fields,
id: fields?.id || new ObjectID.default().toHexString(),
@@ -135,6 +123,8 @@ export function $createBlockNode(fields: Exclude): BlockNode
})
}
-export function $isBlockNode(node: BlockNode | LexicalNode | null | undefined): node is BlockNode {
- return node instanceof BlockNode
+export function $isServerBlockNode(
+ node: LexicalNode | ServerBlockNode | null | undefined,
+): node is ServerBlockNode {
+ return node instanceof ServerBlockNode
}
diff --git a/packages/richtext-lexical/src/features/blocks/nodes/InlineBlocksNode.tsx b/packages/richtext-lexical/src/features/blocks/server/nodes/InlineBlocksNode.tsx
similarity index 65%
rename from packages/richtext-lexical/src/features/blocks/nodes/InlineBlocksNode.tsx
rename to packages/richtext-lexical/src/features/blocks/server/nodes/InlineBlocksNode.tsx
index af532511944..f44e65ae626 100644
--- a/packages/richtext-lexical/src/features/blocks/nodes/InlineBlocksNode.tsx
+++ b/packages/richtext-lexical/src/features/blocks/server/nodes/InlineBlocksNode.tsx
@@ -1,18 +1,16 @@
import type {
DOMConversionMap,
DOMExportOutput,
- EditorConfig,
- LexicalEditor,
LexicalNode,
NodeKey,
SerializedLexicalNode,
Spread,
} from 'lexical'
+import type React from 'react'
import ObjectID from 'bson-objectid'
import { DecoratorNode } from 'lexical'
import { deepCopyObjectSimple } from 'payload/shared'
-import React, { type JSX } from 'react'
export type InlineBlockFields = {
/** Block form data */
@@ -22,13 +20,7 @@ export type InlineBlockFields = {
id: string
}
-const InlineBlockComponent = React.lazy(() =>
- import('../componentInline/index.js').then((module) => ({
- default: module.InlineBlockComponent,
- })),
-)
-
-export type SerializedInlineBlockNode = Spread<
+export type SerializedServerInlineBlockNode = Spread<
{
children?: never // required so that our typed editor state doesn't automatically add children
fields: InlineBlockFields
@@ -37,7 +29,7 @@ export type SerializedInlineBlockNode = Spread<
SerializedLexicalNode
>
-export class InlineBlockNode extends DecoratorNode {
+export class ServerInlineBlockNode extends DecoratorNode {
__fields: InlineBlockFields
constructor({ fields, key }: { fields: InlineBlockFields; key?: NodeKey }) {
@@ -45,8 +37,8 @@ export class InlineBlockNode extends DecoratorNode {
this.__fields = fields
}
- static clone(node: InlineBlockNode): InlineBlockNode {
- return new InlineBlockNode({
+ static clone(node: ServerInlineBlockNode): ServerInlineBlockNode {
+ return new this({
fields: node.__fields,
key: node.__key,
})
@@ -60,8 +52,8 @@ export class InlineBlockNode extends DecoratorNode {
return {}
}
- static importJSON(serializedNode: SerializedInlineBlockNode): InlineBlockNode {
- const node = $createInlineBlockNode(serializedNode.fields)
+ static importJSON(serializedNode: SerializedServerInlineBlockNode): ServerInlineBlockNode {
+ const node = $createServerInlineBlockNode(serializedNode.fields)
return node
}
@@ -79,10 +71,6 @@ export class InlineBlockNode extends DecoratorNode {
return element
}
- decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
- return