Skip to content

Commit ea5eed8

Browse files
authored
šŸ› fix(editor): prevent crash when toggling enableInputMarkdown setting (lobehub#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
1 parent 1eff864 commit ea5eed8

File tree

4 files changed

+72
-26
lines changed

4 files changed

+72
-26
lines changed

ā€Žsrc/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsxā€Ž

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Editor, useEditor } from '@lobehub/editor/react';
1111
import { Flexbox, Icon, Text } from '@lobehub/ui';
1212
import { Card } from 'antd';
1313
import { Clock } from 'lucide-react';
14-
import { memo, useCallback, useEffect, useRef } from 'react';
14+
import { type RefObject, memo, useCallback, useEffect, useRef } from 'react';
1515
import { useTranslation } from 'react-i18next';
1616

1717
interface CronJobContentEditorProps {
@@ -20,8 +20,12 @@ interface CronJobContentEditorProps {
2020
onChange: (value: string) => void;
2121
}
2222

23-
const CronJobContentEditor = memo<CronJobContentEditorProps>(
24-
({ enableRichRender, initialValue, onChange }) => {
23+
interface CronJobContentEditorInnerProps extends CronJobContentEditorProps {
24+
contentRef: RefObject<string>;
25+
}
26+
27+
const CronJobContentEditorInner = memo<CronJobContentEditorInnerProps>(
28+
({ enableRichRender, initialValue, onChange, contentRef }) => {
2529
const { t } = useTranslation('setting');
2630
const editor = useEditor();
2731
const currentValueRef = useRef(initialValue);
@@ -31,23 +35,6 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
3135
currentValueRef.current = initialValue;
3236
}, [initialValue]);
3337

34-
// Initialize editor content when editor is ready
35-
useEffect(() => {
36-
if (!editor) return;
37-
try {
38-
setTimeout(() => {
39-
if (initialValue) {
40-
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
41-
}
42-
}, 100);
43-
} catch (error) {
44-
console.error('[CronJobContentEditor] Failed to initialize editor content:', error);
45-
setTimeout(() => {
46-
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
47-
}, 100);
48-
}
49-
}, [editor, enableRichRender, initialValue]);
50-
5138
// Handle content changes
5239
const handleContentChange = useCallback(
5340
(e: any) => {
@@ -57,13 +44,18 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
5744

5845
const finalContent = nextContent || '';
5946

47+
// Save to parent ref for restoration
48+
if (contentRef) {
49+
(contentRef as { current: string }).current = finalContent;
50+
}
51+
6052
// Only call onChange if content actually changed
6153
if (finalContent !== currentValueRef.current) {
6254
currentValueRef.current = finalContent;
6355
onChange(finalContent);
6456
}
6557
},
66-
[enableRichRender, onChange],
58+
[enableRichRender, onChange, contentRef],
6759
);
6860

6961
return (
@@ -82,6 +74,14 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
8274
content={''}
8375
editor={editor}
8476
lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
77+
onInit={(editor) => {
78+
// Restore content from parent ref when editor re-initializes
79+
if (contentRef?.current) {
80+
editor.setDocument(enableRichRender ? 'markdown' : 'text', contentRef.current);
81+
} else if (initialValue) {
82+
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
83+
}
84+
}}
8585
onTextChange={handleContentChange}
8686
placeholder={t('agentCronJobs.form.content.placeholder')}
8787
plugins={
@@ -108,4 +108,17 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
108108
},
109109
);
110110

111+
const CronJobContentEditor = (props: CronJobContentEditorProps) => {
112+
// Ref to persist content across re-mounts when enableRichRender changes
113+
const contentRef = useRef<string>(props.initialValue);
114+
115+
return (
116+
<CronJobContentEditorInner
117+
contentRef={contentRef}
118+
key={`editor-${props.enableRichRender}`}
119+
{...props}
120+
/>
121+
);
122+
};
123+
111124
export default CronJobContentEditor;

ā€Žsrc/features/ChatInput/ChatInputProvider.tsxā€Ž

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { useEditor } from '@lobehub/editor/react';
2-
import { type ReactNode, memo, useRef } from 'react';
2+
import { type MutableRefObject, type ReactNode, memo, useRef } from 'react';
3+
4+
import { useUserStore } from '@/store/user';
5+
import { labPreferSelectors } from '@/store/user/selectors';
36

47
import StoreUpdater, { type StoreUpdaterProps } from './StoreUpdater';
58
import { Provider, createStore } from './store';
@@ -8,10 +11,16 @@ interface ChatInputProviderProps extends StoreUpdaterProps {
811
children: ReactNode;
912
}
1013

11-
export const ChatInputProvider = memo<ChatInputProviderProps>(
14+
interface ChatInputProviderInnerProps extends StoreUpdaterProps {
15+
children: ReactNode;
16+
contentRef: MutableRefObject<string>;
17+
}
18+
19+
const ChatInputProviderInner = memo<ChatInputProviderInnerProps>(
1220
({
1321
agentId,
1422
children,
23+
contentRef,
1524
leftActions,
1625
rightActions,
1726
mobile,
@@ -31,6 +40,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
3140
createStore={() =>
3241
createStore({
3342
allowExpand,
43+
contentRef,
3444
editor,
3545
leftActions,
3646
mentionItems,
@@ -60,3 +70,13 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
6070
);
6171
},
6272
);
73+
74+
export const ChatInputProvider = (props: ChatInputProviderProps) => {
75+
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
76+
// Ref to persist content across re-mounts when enableRichRender changes
77+
const contentRef = useRef<string>('');
78+
79+
return (
80+
<ChatInputProviderInner contentRef={contentRef} key={`editor-${enableRichRender}`} {...props} />
81+
);
82+
};

ā€Žsrc/features/ChatInput/InputEditor/index.tsxā€Ž

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ const className = cx(css`
3737
`);
3838

3939
const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
40-
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems] =
40+
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems, contentRef] =
4141
useChatInputStore((s) => [
4242
s.editor,
4343
s.slashMenuRef,
4444
s.handleSendButton,
4545
s.updateMarkdownContent,
4646
s.expand,
4747
s.mentionItems,
48+
s.contentRef,
4849
]);
4950

5051
const storeApi = useStoreApi();
@@ -151,7 +152,11 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
151152
onBlur={() => {
152153
disableScope(HotkeyEnum.AddUserMessage);
153154
}}
154-
onChange={() => {
155+
onChange={(e) => {
156+
// Save content to parent ref for restoration when enableRichRender changes
157+
if (contentRef) {
158+
contentRef.current = e.getDocument('markdown') as unknown as string;
159+
}
155160
updateMarkdownContent();
156161
}}
157162
onCompositionEnd={() => {
@@ -177,7 +182,13 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
177182
onFocus={() => {
178183
enableScope(HotkeyEnum.AddUserMessage);
179184
}}
180-
onInit={(editor) => storeApi.setState({ editor })}
185+
onInit={(editor) => {
186+
storeApi.setState({ editor });
187+
// Restore content from parent ref when editor re-initializes
188+
if (contentRef?.current) {
189+
editor.setDocument('markdown', contentRef.current);
190+
}
191+
}}
181192
onPressEnter={({ event: e }) => {
182193
if (e.shiftKey || isChineseInput.current) return;
183194
// when user like alt + enter to add ai message

ā€Žsrc/features/ChatInput/store/initialState.tsā€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type IEditor, type SlashOptions } from '@lobehub/editor';
22
import type { ChatInputProps } from '@lobehub/editor/react';
33
import type { MenuProps } from '@lobehub/ui';
4+
import type { MutableRefObject } from 'react';
45

56
import { type ActionKeys } from '@/features/ChatInput';
67

@@ -39,6 +40,7 @@ export interface PublicState {
3940
}
4041

4142
export interface State extends PublicState {
43+
contentRef?: MutableRefObject<string>;
4244
editor?: IEditor;
4345
isContentEmpty: boolean;
4446
markdownContent: string;

0 commit comments

Comments
Ā (0)