mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 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:
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user