From 369b3fe46def9198daf722f2c7c6eb752be20c7f Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Fri, 29 Aug 2025 16:53:41 -0700 Subject: [PATCH] new, first-party import type textToEditorState helper --- .../src/exports/client/index.ts | 1 + .../RenderLexical/RichTextComponentClient.tsx | 42 +------- packages/richtext-lexical/src/index.ts | 1 + .../src/utilities/textToEditorState.ts | 99 +++++++++++++++++++ test/access-control/config.ts | 23 ++--- .../LexicalLocalized/textToLexicalJSON.ts | 60 ----------- .../lexical/collections/OnDemand/OnDemand.tsx | 2 +- .../collections/OnDemand/OnDemand2.tsx | 6 +- test/lexical/lexical.int.spec.ts | 18 ++-- test/versions/seed.ts | 6 +- test/versions/textToLexicalJSON.ts | 41 -------- 11 files changed, 133 insertions(+), 166 deletions(-) create mode 100644 packages/richtext-lexical/src/utilities/textToEditorState.ts delete mode 100644 test/lexical/collections/LexicalLocalized/textToLexicalJSON.ts delete mode 100644 test/versions/textToLexicalJSON.ts diff --git a/packages/richtext-lexical/src/exports/client/index.ts b/packages/richtext-lexical/src/exports/client/index.ts index f3f03160f9..45b16289da 100644 --- a/packages/richtext-lexical/src/exports/client/index.ts +++ b/packages/richtext-lexical/src/exports/client/index.ts @@ -152,3 +152,4 @@ export { useBlockComponentContext } from '../../features/blocks/client/component export { getRestPopulateFn } from '../../features/converters/utilities/restPopulateFn.js' export { useRenderEditor_internal_ } from '../../field/RenderLexical/useRenderEditor.js' +export { textToEditorState } from '../../utilities/textToEditorState.js' diff --git a/packages/richtext-lexical/src/field/RenderLexical/RichTextComponentClient.tsx b/packages/richtext-lexical/src/field/RenderLexical/RichTextComponentClient.tsx index df9b58334c..536eac35a6 100644 --- a/packages/richtext-lexical/src/field/RenderLexical/RichTextComponentClient.tsx +++ b/packages/richtext-lexical/src/field/RenderLexical/RichTextComponentClient.tsx @@ -1,48 +1,11 @@ 'use client' -import type { SerializedEditorState } from 'lexical' import type { FormState } from 'payload' import { Form } from '@payloadcms/ui' import React from 'react' -import type { SerializedParagraphNode, SerializedTextNode } from '../../nodeTypes.js' - -export function textToLexicalJSON({ text }: { text: string }): SerializedEditorState { - const editorJSON: SerializedEditorState = { - root: { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { - type: 'text', - detail: 0, - format: 0, - mode: 'normal', - style: '', - text, - version: 1, - } as SerializedTextNode, - ], - direction: 'ltr', - format: '', - indent: 0, - textFormat: 0, - textStyle: '', - version: 1, - } as SerializedParagraphNode, - ], - direction: 'ltr', - format: '', - indent: 0, - version: 1, - }, - } - - return editorJSON -} +import { textToEditorState } from '../../utilities/textToEditorState.js' export const RichTextComponentClient: React.FC<{ FieldComponent: React.ReactNode @@ -50,7 +13,8 @@ export const RichTextComponentClient: React.FC<{ const { FieldComponent } = props const [initialState] = React.useState(() => { - const lexical = textToLexicalJSON({ text: 'Hello world' }) + const lexical = textToEditorState({ text: 'Hello world' }) + return { richText: { initialValue: lexical, diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index 1256d68880..1748d31c4f 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -1068,4 +1068,5 @@ export { propsToJSXString, } from './utilities/jsx/jsx.js' +export { textToEditorState } from './utilities/textToEditorState.js' export { upgradeLexicalData } from './utilities/upgradeLexicalData/index.js' diff --git a/packages/richtext-lexical/src/utilities/textToEditorState.ts b/packages/richtext-lexical/src/utilities/textToEditorState.ts new file mode 100644 index 0000000000..9346403d3e --- /dev/null +++ b/packages/richtext-lexical/src/utilities/textToEditorState.ts @@ -0,0 +1,99 @@ +import type { SerializedLexicalNode } from 'lexical' + +import type { DefaultTypedEditorState, TypedEditorState } from '../nodeTypes.js' + +export function textToEditorState(args: { + nodes?: DefaultTypedEditorState['root']['children'] + text?: string +}): DefaultTypedEditorState + +export function textToEditorState(args: { + // If you pass children typed for a specific schema T, the return is TypedEditorState + nodes?: TypedEditorState['root']['children'] + text?: string +}): TypedEditorState + +/** + * Helper to build lexical editor state JSON from text and/or nodes. + * + * @param nodes - The nodes to include in the editor state. If you pass the `text` argument, this will append your nodes after the first paragraph node. + * @param text - The text content to include in the editor state. This will create a paragraph node with a text node for you and set that as the first node. + * @returns The constructed editor state JSON. + * + * @example + * + * just passing text: + * + * ```ts + * const editorState = textToEditorState({ text: 'Hello world' }) // result typed as DefaultTypedEditorState + * ``` + * + * @example + * + * passing nodes: + * + * ```ts + * const editorState = // result typed as TypedEditorState (or TypedEditorState) + * textToEditorState({ // or just textToEditorState if you *only* want to allow block nodes + * nodes: [ + * { + * type: 'block', + * fields: { + * id: 'id', + * blockName: 'Cool block', + * blockType: 'myBlock', + * }, + * format: 'left', + * version: 1, + * } + * ], + * }) + * ``` + */ +export function textToEditorState({ + nodes, + text, +}: { + nodes?: DefaultTypedEditorState['root']['children'] | TypedEditorState['root']['children'] + text?: string +}): DefaultTypedEditorState | TypedEditorState { + const editorJSON: DefaultTypedEditorState = { + root: { + type: 'root', + children: [], + direction: 'ltr', + format: '', + indent: 0, + version: 1, + }, + } + + if (text) { + editorJSON.root.children.push({ + type: 'paragraph', + children: [ + { + type: 'text', + detail: 0, + format: 0, + mode: 'normal', + style: '', + text, + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + textStyle: '', + version: 1, + }) + } + + if (nodes?.length) { + editorJSON.root.children.push(...(nodes as any)) + } + + return editorJSON +} diff --git a/test/access-control/config.ts b/test/access-control/config.ts index 3081766ca0..ba59a7214c 100644 --- a/test/access-control/config.ts +++ b/test/access-control/config.ts @@ -4,11 +4,12 @@ const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) import type { FieldAccess } from 'payload' +import { textToEditorState } from '@payloadcms/richtext-lexical' + import type { Config, User } from './payload-types.js' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' -import { textToLexicalJSON } from '../lexical/collections/LexicalLocalized/textToLexicalJSON.js' import { Auth } from './collections/Auth/index.js' import { Disabled } from './collections/Disabled/index.js' import { Hooks } from './collections/hooks/index.js' @@ -718,33 +719,33 @@ export default buildConfigWithDefaults( await payload.create({ collection: 'regression1', data: { - richText4: textToLexicalJSON({ text: 'Text1' }), - array: [{ art: textToLexicalJSON({ text: 'Text2' }) }], - arrayWithAccessFalse: [{ richText6: textToLexicalJSON({ text: 'Text3' }) }], + richText4: textToEditorState({ text: 'Text1' }), + array: [{ art: textToEditorState({ text: 'Text2' }) }], + arrayWithAccessFalse: [{ richText6: textToEditorState({ text: 'Text3' }) }], group1: { text: 'Text4', - richText1: textToLexicalJSON({ text: 'Text5' }), + richText1: textToEditorState({ text: 'Text5' }), }, blocks: [ { blockType: 'myBlock3', - richText7: textToLexicalJSON({ text: 'Text6' }), + richText7: textToEditorState({ text: 'Text6' }), blockName: 'My Block 1', }, ], blocks3: [ { blockType: 'myBlock2', - richText5: textToLexicalJSON({ text: 'Text7' }), + richText5: textToEditorState({ text: 'Text7' }), blockName: 'My Block 2', }, ], tab1: { - richText2: textToLexicalJSON({ text: 'Text8' }), + richText2: textToEditorState({ text: 'Text8' }), blocks2: [ { blockType: 'myBlock', - richText3: textToLexicalJSON({ text: 'Text9' }), + richText3: textToEditorState({ text: 'Text9' }), blockName: 'My Block 3', }, ], @@ -757,12 +758,12 @@ export default buildConfigWithDefaults( data: { array: [ { - richText2: textToLexicalJSON({ text: 'Text1' }), + richText2: textToEditorState({ text: 'Text1' }), }, ], group: { text: 'Text2', - richText1: textToLexicalJSON({ text: 'Text3' }), + richText1: textToEditorState({ text: 'Text3' }), }, }, }) diff --git a/test/lexical/collections/LexicalLocalized/textToLexicalJSON.ts b/test/lexical/collections/LexicalLocalized/textToLexicalJSON.ts deleted file mode 100644 index e7a2ae4676..0000000000 --- a/test/lexical/collections/LexicalLocalized/textToLexicalJSON.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { SerializedRelationshipNode } from '@payloadcms/richtext-lexical' -import type { - SerializedEditorState, - SerializedParagraphNode, - SerializedTextNode, -} from '@payloadcms/richtext-lexical/lexical' - -import { lexicalLocalizedFieldsSlug } from '../../slugs.js' - -export function textToLexicalJSON({ - text, - lexicalLocalizedRelID, -}: { - lexicalLocalizedRelID?: number | string - text: string -}): any { - 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, - textFormat: 0, - type: 'paragraph', - textStyle: '', - version: 1, - } as SerializedParagraphNode, - ], - }, - } - - if (lexicalLocalizedRelID) { - editorJSON.root.children.push({ - format: '', - type: 'relationship', - version: 2, - relationTo: lexicalLocalizedFieldsSlug, - value: lexicalLocalizedRelID, - } as SerializedRelationshipNode) - } - - return editorJSON -} diff --git a/test/lexical/collections/OnDemand/OnDemand.tsx b/test/lexical/collections/OnDemand/OnDemand.tsx index 7ec3f8502d..318747dafa 100644 --- a/test/lexical/collections/OnDemand/OnDemand.tsx +++ b/test/lexical/collections/OnDemand/OnDemand.tsx @@ -18,5 +18,5 @@ export const OnDemand: React.FC = () => { void renderLexical() mounted.current = true }, [renderLexical]) - return
Component: {Component ? Component : 'Loading...'}
+ return
Default Component: {Component ? Component : 'Loading...'}
} diff --git a/test/lexical/collections/OnDemand/OnDemand2.tsx b/test/lexical/collections/OnDemand/OnDemand2.tsx index 20669c2a1e..d8fc3e20e5 100644 --- a/test/lexical/collections/OnDemand/OnDemand2.tsx +++ b/test/lexical/collections/OnDemand/OnDemand2.tsx @@ -1,5 +1,7 @@ 'use client' +import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical' + import { useRenderEditor_internal_ } from '@payloadcms/richtext-lexical/client' import { useEffect, useRef } from 'react' @@ -9,7 +11,7 @@ export const OnDemand: React.FC = () => { const { Component, renderLexical } = useRenderEditor_internal_({ name: 'richText', editorTarget: `collections.${lexicalFullyFeaturedSlug}.richText`, - initialState: {} as any, + initialState: {} as DefaultTypedEditorState, }) const mounted = useRef(false) @@ -20,5 +22,5 @@ export const OnDemand: React.FC = () => { void renderLexical() mounted.current = true }, [renderLexical]) - return
Component: {Component ? Component : 'Loading...'}
+ return
Fully-Featured Component: {Component ? Component : 'Loading...'}
} diff --git a/test/lexical/lexical.int.spec.ts b/test/lexical/lexical.int.spec.ts index 1c06b3c949..2e6878cfb6 100644 --- a/test/lexical/lexical.int.spec.ts +++ b/test/lexical/lexical.int.spec.ts @@ -1,16 +1,17 @@ -/* eslint-disable jest/no-conditional-in-test */ -import type { - SerializedBlockNode, - SerializedLinkNode, - SerializedRelationshipNode, - SerializedUploadNode, -} from '@payloadcms/richtext-lexical' import type { SerializedEditorState, SerializedParagraphNode, } from '@payloadcms/richtext-lexical/lexical' import type { PaginatedDocs, Payload } from 'payload' +/* eslint-disable jest/no-conditional-in-test */ +import { + type SerializedBlockNode, + type SerializedLinkNode, + type SerializedRelationshipNode, + type SerializedUploadNode, + textToEditorState, +} from '@payloadcms/richtext-lexical' import path from 'path' import { fileURLToPath } from 'url' @@ -21,7 +22,6 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js' import { NextRESTClient } from '../helpers/NextRESTClient.js' import { lexicalDocData } from './collections/Lexical/data.js' import { generateLexicalLocalizedRichText } from './collections/LexicalLocalized/generateLexicalRichText.js' -import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js' import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js' import { richTextDocData } from './collections/RichText/data.js' import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText.js' @@ -655,7 +655,7 @@ describe('Lexical', () => { locale: 'en', data: { title: 'Localized Lexical hooks', - lexicalBlocksLocalized: textToLexicalJSON({ text: 'some text' }), + lexicalBlocksLocalized: textToEditorState({ text: 'some text' }), lexicalBlocksSubLocalized: generateLexicalLocalizedRichText( 'Shared text', 'English text in block', diff --git a/test/versions/seed.ts b/test/versions/seed.ts index e7271cc109..95e5633d77 100644 --- a/test/versions/seed.ts +++ b/test/versions/seed.ts @@ -1,3 +1,4 @@ +import { textToEditorState } from '@payloadcms/richtext-lexical' import path from 'path' import { getFileByPath, type Payload } from 'payload' import { fileURLToPath } from 'url' @@ -14,7 +15,6 @@ import { media2CollectionSlug, mediaCollectionSlug, } from './slugs.js' -import { textToLexicalJSON } from './textToLexicalJSON.js' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -269,7 +269,7 @@ export async function seed(_payload: Payload, parallel: boolean = false) { textID: doc1ID, updated: false, }) as any, - richtextWithCustomDiff: textToLexicalJSON({ text: 'richtextWithCustomDiff' }), + richtextWithCustomDiff: textToEditorState({ text: 'richtextWithCustomDiff' }), select: 'option1', text: 'text', textArea: 'textArea', @@ -431,7 +431,7 @@ export async function seed(_payload: Payload, parallel: boolean = false) { textID: doc2ID, updated: true, }) as any, - richtextWithCustomDiff: textToLexicalJSON({ text: 'richtextWithCustomDiff2' }), + richtextWithCustomDiff: textToEditorState({ text: 'richtextWithCustomDiff2' }), select: 'option2', text: 'text2', textArea: 'textArea2', diff --git a/test/versions/textToLexicalJSON.ts b/test/versions/textToLexicalJSON.ts deleted file mode 100644 index c2f12c9e50..0000000000 --- a/test/versions/textToLexicalJSON.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { - SerializedEditorState, - SerializedParagraphNode, - SerializedTextNode, -} from '@payloadcms/richtext-lexical/lexical' - -export function textToLexicalJSON({ text }: { text: string }): any { - 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, - textFormat: 0, - type: 'paragraph', - textStyle: '', - version: 1, - } as SerializedParagraphNode, - ], - }, - } - - return editorJSON -}