fix(richtext-lexical): recursively unwrap generic Slate nodes in Lexical migration converter (#13202)
## What? The Slate to Lexical migration script assumes that the depth of Slate nodes matches the depth of the Lexical schema, which isn't necessarily true. This pull request fixes this assumption by first checking for children and unwrapping the text nodes. ## Why? During my migration, I ran into a lot of copy + pasted rich text with list items with untyped nodes with `children`. The existing migration script assumed that since list items can't have paragraphs, all untyped nodes inside must be text nodes. The result of the migration script was a lot of invalid text nodes with `text: undefined` and all of the content in the `children` being silently lost. Beyond the silent loss, the invalid text nodes caused the Lexical editor to unmount with an error about accessing `0 of undefined`, so those documents couldn't be edited. This additionally makes the migration script more closely align with the [recursive serialization logic recommendation from the Payload Slate Rich Text documentation](https://payloadcms.com/docs/rich-text/slate#generating-html). ## Visualization ### Slate ```txt Slate rich text content ┣━┳━ Unordered list ┋ ┣━┳━ List item ┋ ┋ ┗━┳━ Generic (paragraph-like, untyped with children) ┋ ┋ ┣━━━ Text (untyped) `Hello ` ┋ ┋ ┗━━━ Text (untyped) `World! [...] ``` ### Lexical Before PR ```txt Lexical rich text content (invalid) ┣━┳━ Unordered list ┋ ┣━┳━ List item ┋ ┋ ┗━━━ Invalid text (assumed the generic node was text, stopped processing children, cannot restore lost text without a restoring backup with Slate and rerunning the script after this MR) [...] ``` ### Lexical After PR ```txt Lexical rich text content ┣━┳━ Unordered list ┋ ┣━┳━ List item ┋ ┋ ┣━━━ Text `Hello ` ┋ ┋ ┗━━━ Text `World! [...] ``` --------- Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
This commit is contained in:
@@ -53,12 +53,23 @@ export function convertSlateNodesToLexical({
|
||||
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
|
||||
// @ts-expect-error - vestiges of the migration to strict mode. Probably not important enough in this file to fix
|
||||
return (
|
||||
slateNodes.map((slateNode, i) => {
|
||||
// Flatten in case we unwrap an array of child nodes
|
||||
slateNodes.flatMap((slateNode, i) => {
|
||||
if (!('type' in slateNode)) {
|
||||
if (canContainParagraphs) {
|
||||
// This is a paragraph node. They do not have a type property in Slate
|
||||
return convertParagraphNode(converters, slateNode)
|
||||
} else {
|
||||
// Unwrap generic Slate nodes recursively since depth wasn't guaranteed by Slate, especially when copy + pasting rich text
|
||||
// - If there are children and it can't be a paragraph in Lexical, assume that the generic node should be unwrapped until the text nodes, and only assume that its a text node when there are no more children
|
||||
if (slateNode.children) {
|
||||
return convertSlateNodesToLexical({
|
||||
canContainParagraphs,
|
||||
converters,
|
||||
parentNodeType,
|
||||
slateNodes: slateNode.children || [],
|
||||
})
|
||||
}
|
||||
// This is a simple text node. canContainParagraphs may be false if this is nested inside a paragraph already, since paragraphs cannot contain paragraphs
|
||||
return convertTextNode(slateNode)
|
||||
}
|
||||
@@ -113,7 +124,7 @@ export function convertTextNode(node: SlateNode): SerializedTextNode {
|
||||
format: convertNodeToFormat(node),
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: node.text,
|
||||
text: node.text ?? "",
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,16 @@ export function generateSlateRichText() {
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: "It's built with SlateJS",
|
||||
// This node is untyped, because I want to test this scenario:
|
||||
// https://github.com/payloadcms/payload/pull/13202
|
||||
children: [
|
||||
{
|
||||
text: 'This editor is built ',
|
||||
},
|
||||
{
|
||||
text: 'with SlateJS',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
type: 'li',
|
||||
|
||||
Reference in New Issue
Block a user