Skip to content

Commit

Permalink
feat(richtext-lexical): fully-typed blocks in JSX serializer (payload…
Browse files Browse the repository at this point in the history
…cms#9554)

This allows for full type-safety when using our official JSX converter
with blocks:

![CleanShot 2024-11-26 at 21 35
18@2x](https://github.com/user-attachments/assets/70ceb3e9-d5d1-4074-a5dd-bb9d514dc229)

![CleanShot 2024-11-26 at 21 35
25@2x](https://github.com/user-attachments/assets/5100133c-8a91-4cfe-8e44-c091b2d86ffa)
  • Loading branch information
AlessioGr authored Nov 27, 2024
1 parent b47ebb6 commit 519bb79
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DefaultNodeTypes } from '../../../../../nodeTypes.js'
import type { JSXConverters } from './types.js'

import { BlockquoteJSXConverter } from './converters/blockquote.js'
Expand All @@ -11,7 +12,7 @@ import { TableJSXConverter } from './converters/table.js'
import { TextJSXConverter } from './converters/text.js'
import { UploadJSXConverter } from './converters/upload.js'

export const defaultJSXConverters: JSXConverters = {
export const defaultJSXConverters: JSXConverters<DefaultNodeTypes> = {
...ParagraphJSXConverter,
...TextJSXConverter,
...LinebreakJSXConverter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,53 @@ export type JSXConverter<T extends { [key: string]: any; type?: string } = Seria
}) => React.ReactNode[]
parent: SerializedLexicalNodeWithParent
}) => React.ReactNode
export type JSXConverters<T extends { [key: string]: any; type?: string } = DefaultNodeTypes> = {

export type JSXConverters<
T extends { [key: string]: any; type?: string } =
| DefaultNodeTypes
| SerializedBlockNode<{ blockName?: null | string; blockType: string }> // need these to ensure types for blocks and inlineBlocks work if no generics are provided
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>, // need these to ensure types for blocks and inlineBlocks work if no generics are provided
> = {
[key: string]:
| {
[blockSlug: string]: JSXConverter<any> // Not true, but need to appease TypeScript
[blockSlug: string]: JSXConverter<any>
}
| JSXConverter<any>
| undefined
} & {
[nodeType in NonNullable<T['type']>]?: JSXConverter<Extract<T, { type: nodeType }>>
[nodeType in Exclude<NonNullable<T['type']>, 'block' | 'inlineBlock'>]?: JSXConverter<
Extract<T, { type: nodeType }>
>
} & {
blocks?: {
[blockSlug: string]: JSXConverter<{ fields: Record<string, any> } & SerializedBlockNode>
[K in Extract<
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
? B extends { blockType: string }
? B['blockType']
: never
: never,
string
>]?: JSXConverter<
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
? SerializedBlockNode<Extract<B, { blockType: K }>>
: SerializedBlockNode
>
}
inlineBlocks?: {
[blockSlug: string]: JSXConverter<{ fields: Record<string, any> } & SerializedInlineBlockNode>
[K in Extract<
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
? B extends { blockType: string }
? B['blockType']
: never
: never,
string
>]?: JSXConverter<
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
? SerializedInlineBlockNode<Extract<B, { blockType: K }>>
: SerializedInlineBlockNode
>
}
}

export type SerializedLexicalNodeWithParent = {
parent?: SerializedLexicalNode
} & SerializedLexicalNode
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@ import type { SerializedEditorState } from 'lexical'

import React from 'react'

import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '../../../../nodeTypes.js'
import type { JSXConverters } from './converter/types.js'

import { defaultJSXConverters } from './converter/defaultConverters.js'
import { convertLexicalToJSX } from './converter/index.js'

export type JSXConvertersFunction = (args: { defaultConverters: JSXConverters }) => JSXConverters
export type JSXConvertersFunction<
T extends { [key: string]: any; type?: string } =
| DefaultNodeTypes
| SerializedBlockNode<{ blockName?: null | string; blockType: string }>
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>,
> = (args: { defaultConverters: JSXConverters<DefaultNodeTypes> }) => JSXConverters<T>

type Props = {
className?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import './index.scss'

import { v4 as uuid } from 'uuid'

import type { InlineBlockFields } from '../nodes/InlineBlocksNode.js'
import type { InlineBlockFields } from '../../server/nodes/InlineBlocksNode.js'

import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
import { useLexicalDrawer } from '../../../../utilities/fieldsDrawer/useLexicalDrawer.js'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,22 @@
'use client'
import type {
EditorConfig,
LexicalEditor,
LexicalNode,
SerializedLexicalNode,
Spread,
} from 'lexical'
import type { EditorConfig, LexicalEditor, LexicalNode } from 'lexical'

import ObjectID from 'bson-objectid'
import React, { type JSX } from 'react'

import type { SerializedServerInlineBlockNode } from '../../server/nodes/InlineBlocksNode.js'
import type {
InlineBlockFields,
SerializedInlineBlockNode,
} from '../../server/nodes/InlineBlocksNode.js'

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 clone(node: ServerInlineBlockNode): ServerInlineBlockNode {
return super.clone(node)
Expand All @@ -55,7 +35,7 @@ export class InlineBlockNode extends ServerInlineBlockNode {
return <InlineBlockComponent formData={this.getFields()} nodeKey={this.getKey()} />
}

exportJSON(): SerializedServerInlineBlockNode {
exportJSON(): SerializedInlineBlockNode {
return super.exportJSON()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Block } from 'payload'

import type { PopulationPromise } from '../../typesServer.js'
import type { SerializedInlineBlockNode } from '../client/nodes/InlineBlocksNode.js'
import type { SerializedInlineBlockNode } from '../server/nodes/InlineBlocksNode.js'
import type { SerializedBlockNode } from './nodes/BlocksNode.js'

import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export type InlineBlockFields<TInlineBlockFields extends JsonObject = JsonObject
id: string
} & TInlineBlockFields

export type SerializedServerInlineBlockNode = Spread<
export type SerializedInlineBlockNode<TBlockFields extends JsonObject = JsonObject> = Spread<
{
children?: never // required so that our typed editor state doesn't automatically add children
fields: InlineBlockFields
fields: InlineBlockFields<TBlockFields>
type: 'inlineBlock'
},
SerializedLexicalNode
Expand Down Expand Up @@ -52,7 +52,7 @@ export class ServerInlineBlockNode extends DecoratorNode<null | React.ReactEleme
return {}
}

static importJSON(serializedNode: SerializedServerInlineBlockNode): ServerInlineBlockNode {
static importJSON(serializedNode: SerializedInlineBlockNode): ServerInlineBlockNode {
const node = $createServerInlineBlockNode(serializedNode.fields)
return node
}
Expand Down Expand Up @@ -84,7 +84,7 @@ export class ServerInlineBlockNode extends DecoratorNode<null | React.ReactEleme
return { element }
}

exportJSON(): SerializedServerInlineBlockNode {
exportJSON(): SerializedInlineBlockNode {
return {
type: 'inlineBlock',
fields: this.getFields(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { Block } from 'payload'
import { fieldSchemasToFormState } from '@payloadcms/ui/forms/fieldSchemasToFormState'

import type { NodeValidation } from '../../typesServer.js'
import type { SerializedInlineBlockNode } from '../client/nodes/InlineBlocksNode.js'
import type { BlockFields, SerializedBlockNode } from './nodes/BlocksNode.js'
import type { SerializedInlineBlockNode } from './nodes/InlineBlocksNode.js'

export const blockValidationHOC = (
blocks: Block[],
Expand Down
2 changes: 1 addition & 1 deletion packages/richtext-lexical/src/nodeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import type {
} from 'lexical'

import type { SerializedQuoteNode } from './features/blockquote/server/index.js'
import type { SerializedInlineBlockNode } from './features/blocks/client/nodes/InlineBlocksNode.js'
import type { SerializedBlockNode } from './features/blocks/server/nodes/BlocksNode.js'
import type { SerializedInlineBlockNode } from './features/blocks/server/nodes/InlineBlocksNode.js'
import type {
SerializedTableCellNode,
SerializedTableNode,
Expand Down

0 comments on commit 519bb79

Please sign in to comment.