Describe the Bug
After inserting a block into a Lexical rich-text field that has BlocksFeature, the first Cmd/Ctrl+Z appears to do nothing: the block card is still there. A second Cmd/Ctrl+Z removes it. Expected: one undo removes the block that was just inserted, as it does for a paragraph, heading or list.
The same happens for a block created from a custom feature with editor.update(() => $insertNodes([$createBlockNode(…)])). We saw it on a text-style menu action that moves paragraphs into a new block: undo #1 changed nothing, and undo #2 restored the paragraphs.
Likely cause. In packages/richtext-lexical/src/features/blocks/client/component/index.tsx, the "Initial state for newly created blocks" effect (on main at 01f44da06, L245–L299; the same code path in v3.88.0 and v3.90.2) handles a block created on the client. It awaits getFormState, then writes the returned values back into the node:
editor.update(
() => {
const node = $getNodeByKey(nodeKey)
if (node && $isBlockNode(node)) {
const newData = newFormStateData
newData.blockType = blockType
node.setFields(newData, true)
}
},
{ tag: SKIP_DOM_SELECTION_TAG },
)
The update dirties the decorator node and carries no history tag. @lexical/history merges an update into the previous entry only when it is tagged history-merge, or when it is a same-type text change within the merge delay. So this write becomes its own entry, after the insertion's. Undo #1 reverts the write, which changes nothing visible because the node keeps its fields. Undo #2 reverts the insertion. componentInline/index.tsx has the same write.
Suggested fix. Merge the write into the insertion's history entry. It is not a user edit; it only fills in what the server resolved:
import { $getNodeByKey, HISTORY_MERGE_TAG, SKIP_DOM_SELECTION_TAG } from 'lexical'
// …
{ tag: [SKIP_DOM_SELECTION_TAG, HISTORY_MERGE_TAG] }
Apply the same in componentInline. One edge case: if the user edits between the insertion and the getFormState response, the write merges into that later entry instead. Undoing that edit would then also revert the defaults, which is still one step per user action.
Link to the code that reproduces this issue
https://github.com/lexsemenenko/payload-lexical-block-undo-repro
Reproduction Steps
The repo is npx create-payload-app@3.90.2 -t blank --db sqlite, plus one posts collection with this one field and nothing else changed:
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({ blocks: [{ slug: 'note', fields: [{ name: 'text', type: 'text' }] }] }),
],
}),
}
pnpm install, then cp .env.example .env (SQLite; the database file is created inside the repo), then pnpm dev (port 3190).
- Open http://localhost:3190/admin and create the first user.
- Posts → Create New. Click into Content, type
Hello, press Enter.
- Type
/note and press Enter. A "Note" block card appears.
- Wait about two seconds, then press Cmd/Ctrl+Z once. The card is still there.
- Press Cmd/Ctrl+Z again. The card is removed.
A Playwright spec (tests/undo-block.spec.ts) automates this. expected: ONE undo removes a just-inserted block fails on 3.90.2 (Expected: 0, Received: 1). The companion test records the card count after each undo: 1, then 0. The output of one run is in repro-run.txt.
Which area(s) are affected?
plugin: richtext-lexical
Environment Info
payload: 3.90.2
@payloadcms/richtext-lexical: 3.90.2
@payloadcms/next: 3.90.2
@payloadcms/ui: 3.90.2
@payloadcms/db-sqlite: 3.90.2
next: 16.3.3
react / react-dom: 19.2.6
lexical / @lexical/history / @lexical/react: 0.50.0
Node: 24.14.1
pnpm: 10.33.2
Browser: Chromium (via @playwright/test 1.58.2); also seen by hand in Chrome
First observed on payload 3.88.0 / lexical 0.41.0 / next 16.2.12; the code path is unchanged on main at 01f44da06.
Describe the Bug
After inserting a block into a Lexical rich-text field that has
BlocksFeature, the first Cmd/Ctrl+Z appears to do nothing: the block card is still there. A second Cmd/Ctrl+Z removes it. Expected: one undo removes the block that was just inserted, as it does for a paragraph, heading or list.The same happens for a block created from a custom feature with
editor.update(() => $insertNodes([$createBlockNode(…)])). We saw it on a text-style menu action that moves paragraphs into a new block: undo #1 changed nothing, and undo #2 restored the paragraphs.Likely cause. In
packages/richtext-lexical/src/features/blocks/client/component/index.tsx, the "Initial state for newly created blocks" effect (onmainat01f44da06, L245–L299; the same code path in v3.88.0 and v3.90.2) handles a block created on the client. It awaitsgetFormState, then writes the returned values back into the node:The update dirties the decorator node and carries no history tag.
@lexical/historymerges an update into the previous entry only when it is taggedhistory-merge, or when it is a same-type text change within the merge delay. So this write becomes its own entry, after the insertion's. Undo #1 reverts the write, which changes nothing visible because the node keeps its fields. Undo #2 reverts the insertion.componentInline/index.tsxhas the same write.Suggested fix. Merge the write into the insertion's history entry. It is not a user edit; it only fills in what the server resolved:
Apply the same in
componentInline. One edge case: if the user edits between the insertion and thegetFormStateresponse, the write merges into that later entry instead. Undoing that edit would then also revert the defaults, which is still one step per user action.Link to the code that reproduces this issue
https://github.com/lexsemenenko/payload-lexical-block-undo-repro
Reproduction Steps
The repo is
npx create-payload-app@3.90.2 -t blank --db sqlite, plus onepostscollection with this one field and nothing else changed:pnpm install, thencp .env.example .env(SQLite; the database file is created inside the repo), thenpnpm dev(port 3190).Hello, press Enter./noteand press Enter. A "Note" block card appears.A Playwright spec (
tests/undo-block.spec.ts) automates this.expected: ONE undo removes a just-inserted blockfails on 3.90.2 (Expected: 0, Received: 1). The companion test records the card count after each undo: 1, then 0. The output of one run is inrepro-run.txt.Which area(s) are affected?
plugin: richtext-lexical
Environment Info