fix(editor): wrap dispatchCommand in editor.update() to prevent read-only errors#129
Conversation
…only errors Lexical's dispatchCommand() triggers command listeners synchronously. When called from a React event handler while an editorState.read() is active on the call stack, mutations (e.g. $toggleLink → splitText) execute in a read-only context, throwing 'Cannot use method in read-only mode.' Wrap all mutating dispatchCommand calls in editor.update() in both floating-link-editor-plugin.tsx and floating-toolbar-plugin.tsx. Add a static analysis test to prevent regressions and document the pattern in .agents/conventions.md. Closes #128 Co-authored-by: Ona <no-reply@ona.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Reviewed and merged. Fix correctly wraps all mutating |
|
✅ UI verification passed — design spec compliance confirmed. Static analysis: No visual changes in this PR. All modifications are behavioral (wrapping Visual verification: Playwright screenshots of the editor page (desktop dark + mobile) show no regressions. Layout, spacing, and component rendering are intact. |
|
✅ Post-merge verification passed. The changes in this PR (editor E2E suite: 35/42 passed, 4 failed, 3 skipped (dependent on a failed test) Ad-hoc smoke tests: All passed
Pre-existing failures (not caused by this PR):
|
Summary
Fixes
Error: Cannot use method in read-only mode.thrown when saving/removing links or formatting text via the floating toolbar.Sentry issue: https://ona-2j.sentry.io/issues/MEMO-5 (4 events, 3 users)
Closes #128
Root Cause
editor.dispatchCommand()triggers command listeners synchronously viatriggerCommandListeners→updateEditorSync. WhenactiveEditor === editor(true during aneditorState.read()callback from the update listener),updateEditorSynctakes a fast path that calls the listener directly without creating a writable context. TheLinkPluginlistener calls$toggleLink→splitText→errorOnReadOnly.Both
floating-link-editor-plugin.tsxandfloating-toolbar-plugin.tsxcallededitor.dispatchCommand()from React event handlers without wrapping ineditor.update().Changes
floating-link-editor-plugin.tsx: WraphandleSaveandhandleRemovedispatchCommand calls ineditor.update()floating-toolbar-plugin.tsx: Wrap all format and link toggle dispatchCommand calls ineditor.update()lexical-dispatch-safety.test.ts: Static analysis test that scans editor files for baredispatchCommand()calls with mutating commands outsideeditor.update().agents/conventions.md: Document the "dispatching mutating commands" pattern to prevent recurrence