🐛 fix(editor): prevent crash when toggling enableInputMarkdown setting (#11755)

Fix "Node TableNode has not been registered" error that occurred when
switching enableInputMarkdown from disabled to enabled.

Root cause: Lexical editor nodes must be registered at creation time.
When enableRichRender toggled, plugins tried to register nodes on an
existing editor instance, causing a crash.

Solution: Use key-based re-mounting with content preservation via ref.
- Outer component holds contentRef to persist content across re-mounts
- Inner component re-mounts when enableRichRender changes (via key)
- Content restored from ref on editor initialization
This commit is contained in:
Innei
2026-01-24 00:11:53 +08:00
committed by GitHub
parent 1eff8646f7
commit ea5eed8bcd
4 changed files with 72 additions and 26 deletions

View File

@@ -11,7 +11,7 @@ import { Editor, useEditor } from '@lobehub/editor/react';
import { Flexbox, Icon, Text } from '@lobehub/ui';
import { Card } from 'antd';
import { Clock } from 'lucide-react';
import { memo, useCallback, useEffect, useRef } from 'react';
import { type RefObject, memo, useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
interface CronJobContentEditorProps {
@@ -20,8 +20,12 @@ interface CronJobContentEditorProps {
onChange: (value: string) => void;
}
const CronJobContentEditor = memo<CronJobContentEditorProps>(
({ enableRichRender, initialValue, onChange }) => {
interface CronJobContentEditorInnerProps extends CronJobContentEditorProps {
contentRef: RefObject<string>;
}
const CronJobContentEditorInner = memo<CronJobContentEditorInnerProps>(
({ enableRichRender, initialValue, onChange, contentRef }) => {
const { t } = useTranslation('setting');
const editor = useEditor();
const currentValueRef = useRef(initialValue);
@@ -31,23 +35,6 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
currentValueRef.current = initialValue;
}, [initialValue]);
// Initialize editor content when editor is ready
useEffect(() => {
if (!editor) return;
try {
setTimeout(() => {
if (initialValue) {
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
}
}, 100);
} catch (error) {
console.error('[CronJobContentEditor] Failed to initialize editor content:', error);
setTimeout(() => {
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
}, 100);
}
}, [editor, enableRichRender, initialValue]);
// Handle content changes
const handleContentChange = useCallback(
(e: any) => {
@@ -57,13 +44,18 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
const finalContent = nextContent || '';
// Save to parent ref for restoration
if (contentRef) {
(contentRef as { current: string }).current = finalContent;
}
// Only call onChange if content actually changed
if (finalContent !== currentValueRef.current) {
currentValueRef.current = finalContent;
onChange(finalContent);
}
},
[enableRichRender, onChange],
[enableRichRender, onChange, contentRef],
);
return (
@@ -82,6 +74,14 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
content={''}
editor={editor}
lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
onInit={(editor) => {
// Restore content from parent ref when editor re-initializes
if (contentRef?.current) {
editor.setDocument(enableRichRender ? 'markdown' : 'text', contentRef.current);
} else if (initialValue) {
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
}
}}
onTextChange={handleContentChange}
placeholder={t('agentCronJobs.form.content.placeholder')}
plugins={
@@ -108,4 +108,17 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
},
);
const CronJobContentEditor = (props: CronJobContentEditorProps) => {
// Ref to persist content across re-mounts when enableRichRender changes
const contentRef = useRef<string>(props.initialValue);
return (
<CronJobContentEditorInner
contentRef={contentRef}
key={`editor-${props.enableRichRender}`}
{...props}
/>
);
};
export default CronJobContentEditor;

View File

@@ -1,5 +1,8 @@
import { useEditor } from '@lobehub/editor/react';
import { type ReactNode, memo, useRef } from 'react';
import { type MutableRefObject, type ReactNode, memo, useRef } from 'react';
import { useUserStore } from '@/store/user';
import { labPreferSelectors } from '@/store/user/selectors';
import StoreUpdater, { type StoreUpdaterProps } from './StoreUpdater';
import { Provider, createStore } from './store';
@@ -8,10 +11,16 @@ interface ChatInputProviderProps extends StoreUpdaterProps {
children: ReactNode;
}
export const ChatInputProvider = memo<ChatInputProviderProps>(
interface ChatInputProviderInnerProps extends StoreUpdaterProps {
children: ReactNode;
contentRef: MutableRefObject<string>;
}
const ChatInputProviderInner = memo<ChatInputProviderInnerProps>(
({
agentId,
children,
contentRef,
leftActions,
rightActions,
mobile,
@@ -31,6 +40,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
createStore={() =>
createStore({
allowExpand,
contentRef,
editor,
leftActions,
mentionItems,
@@ -60,3 +70,13 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
);
},
);
export const ChatInputProvider = (props: ChatInputProviderProps) => {
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
// Ref to persist content across re-mounts when enableRichRender changes
const contentRef = useRef<string>('');
return (
<ChatInputProviderInner contentRef={contentRef} key={`editor-${enableRichRender}`} {...props} />
);
};

View File

@@ -37,7 +37,7 @@ const className = cx(css`
`);
const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems] =
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems, contentRef] =
useChatInputStore((s) => [
s.editor,
s.slashMenuRef,
@@ -45,6 +45,7 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
s.updateMarkdownContent,
s.expand,
s.mentionItems,
s.contentRef,
]);
const storeApi = useStoreApi();
@@ -151,7 +152,11 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
onBlur={() => {
disableScope(HotkeyEnum.AddUserMessage);
}}
onChange={() => {
onChange={(e) => {
// Save content to parent ref for restoration when enableRichRender changes
if (contentRef) {
contentRef.current = e.getDocument('markdown') as unknown as string;
}
updateMarkdownContent();
}}
onCompositionEnd={() => {
@@ -177,7 +182,13 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
onFocus={() => {
enableScope(HotkeyEnum.AddUserMessage);
}}
onInit={(editor) => storeApi.setState({ editor })}
onInit={(editor) => {
storeApi.setState({ editor });
// Restore content from parent ref when editor re-initializes
if (contentRef?.current) {
editor.setDocument('markdown', contentRef.current);
}
}}
onPressEnter={({ event: e }) => {
if (e.shiftKey || isChineseInput.current) return;
// when user like alt + enter to add ai message

View File

@@ -1,6 +1,7 @@
import { type IEditor, type SlashOptions } from '@lobehub/editor';
import type { ChatInputProps } from '@lobehub/editor/react';
import type { MenuProps } from '@lobehub/ui';
import type { MutableRefObject } from 'react';
import { type ActionKeys } from '@/features/ChatInput';
@@ -39,6 +40,7 @@ export interface PublicState {
}
export interface State extends PublicState {
contentRef?: MutableRefObject<string>;
editor?: IEditor;
isContentEmpty: boolean;
markdownContent: string;