Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(richtext-lexical): fully-typed blocks in JSX serializer #9554

Merged
merged 7 commits into from
Nov 27, 2024
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