This is an automated email from the ASF dual-hosted git repository.

robin0716 pushed a commit to branch feat/1.7.2/ui
in repository https://gitbox.apache.org/repos/asf/answer.git

commit d87726bd6e54e4fc88f8338f6f6499f878b00c4f
Author: robin <[email protected]>
AuthorDate: Wed Dec 17 10:38:31 2025 +0800

    refactor(editor): enhance editor components with base props and 
initialization logic
    
    - Introduced BaseEditorProps interface to standardize props across 
MarkdownEditor and RichEditor components.
    - Improved initialization logic in RichEditor and MarkdownEditor to handle 
editor state more effectively.
    - Updated useEditor hook to support initial values and prevent unnecessary 
updates during prop changes.
    - Refactored command methods to utilize dispatch for state changes in 
CodeMirror editor.
---
 ui/src/components/Editor/MarkdownEditor.tsx        |  33 ++--
 ui/src/components/Editor/RichEditor.tsx            | 137 +++++++++-------
 ui/src/components/Editor/ToolBars/chart.tsx        | 181 ---------------------
 ui/src/components/Editor/ToolBars/index.ts         |   2 -
 ui/src/components/Editor/index.tsx                 |   5 +-
 ui/src/components/Editor/types.ts                  |  15 +-
 .../components/Editor/utils/codemirror/commands.ts |  16 +-
 ui/src/components/Editor/utils/index.ts            |  26 ++-
 ui/src/components/Editor/utils/tiptap/events.ts    |   8 +-
 9 files changed, 145 insertions(+), 278 deletions(-)

diff --git a/ui/src/components/Editor/MarkdownEditor.tsx 
b/ui/src/components/Editor/MarkdownEditor.tsx
index fe5d8bb4..068a408f 100644
--- a/ui/src/components/Editor/MarkdownEditor.tsx
+++ b/ui/src/components/Editor/MarkdownEditor.tsx
@@ -21,18 +21,10 @@ import { useEffect, useRef } from 'react';
 
 import { EditorView } from '@codemirror/view';
 
-import { Editor } from './types';
+import { BaseEditorProps } from './types';
 import { useEditor } from './utils';
 
-interface MarkdownEditorProps {
-  value: string;
-  onChange?: (value: string) => void;
-  onFocus?: () => void;
-  onBlur?: () => void;
-  placeholder?: string;
-  autoFocus?: boolean;
-  onEditorReady?: (editor: Editor) => void;
-}
+interface MarkdownEditorProps extends BaseEditorProps {}
 
 const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
   value,
@@ -45,6 +37,7 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
 }) => {
   const editorRef = useRef<HTMLDivElement>(null);
   const lastSyncedValueRef = useRef<string>(value);
+  const isInitializedRef = useRef<boolean>(false);
 
   const editor = useEditor({
     editorRef,
@@ -53,24 +46,20 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
     onBlur,
     placeholder,
     autoFocus,
+    initialValue: value,
   });
 
   useEffect(() => {
-    if (!editor) {
+    if (!editor || isInitializedRef.current) {
       return;
     }
 
-    editor.setValue(value || '');
-    lastSyncedValueRef.current = value || '';
+    isInitializedRef.current = true;
     onEditorReady?.(editor);
-  }, [editor]);
+  }, [editor, onEditorReady]);
 
   useEffect(() => {
-    if (!editor) {
-      return;
-    }
-
-    if (value === lastSyncedValueRef.current) {
+    if (!editor || value === lastSyncedValueRef.current) {
       return;
     }
 
@@ -82,6 +71,9 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
   }, [editor, value]);
 
   useEffect(() => {
+    lastSyncedValueRef.current = value;
+    isInitializedRef.current = false;
+
     return () => {
       if (editor) {
         const view = editor as unknown as EditorView;
@@ -89,8 +81,9 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
           view.destroy();
         }
       }
+      isInitializedRef.current = false;
     };
-  }, [editor]);
+  }, []);
 
   return (
     <div className="content-wrap">
diff --git a/ui/src/components/Editor/RichEditor.tsx 
b/ui/src/components/Editor/RichEditor.tsx
index c574551c..916c8249 100644
--- a/ui/src/components/Editor/RichEditor.tsx
+++ b/ui/src/components/Editor/RichEditor.tsx
@@ -30,18 +30,10 @@ import { Markdown } from '@tiptap/markdown';
 import Image from '@tiptap/extension-image';
 import { TableKit } from '@tiptap/extension-table';
 
-import { Editor } from './types';
+import { Editor, BaseEditorProps } from './types';
 import { createTipTapAdapter } from './utils/tiptap/adapter';
 
-interface RichEditorProps {
-  value: string;
-  onChange?: (value: string) => void;
-  onFocus?: () => void;
-  onBlur?: () => void;
-  placeholder?: string;
-  autoFocus?: boolean;
-  onEditorReady?: (editor: Editor) => void;
-}
+interface RichEditorProps extends BaseEditorProps {}
 
 const RichEditor: React.FC<RichEditorProps> = ({
   value,
@@ -53,14 +45,60 @@ const RichEditor: React.FC<RichEditorProps> = ({
   onEditorReady,
 }) => {
   const lastSyncedValueRef = useRef<string>(value);
-
   const adaptedEditorRef = useRef<Editor | null>(null);
+  const isInitializedRef = useRef<boolean>(false);
+  const isUpdatingFromPropsRef = useRef<boolean>(false);
+  const onEditorReadyRef = useRef(onEditorReady);
+  const autoFocusRef = useRef(autoFocus);
+  const initialValueRef = useRef<string>(value);
+
+  useEffect(() => {
+    onEditorReadyRef.current = onEditorReady;
+    autoFocusRef.current = autoFocus;
+  }, [onEditorReady, autoFocus]);
+
+  const isViewAvailable = (editorInstance: TipTapEditor | null): boolean => {
+    if (!editorInstance) {
+      return false;
+    }
+    if (editorInstance.isDestroyed) {
+      return false;
+    }
+    return !!(editorInstance.view && editorInstance.state);
+  };
+
+  const handleCreate = useCallback(
+    ({ editor: editorInstance }: { editor: TipTapEditor }) => {
+      if (isInitializedRef.current || !isViewAvailable(editorInstance)) {
+        return;
+      }
+
+      isInitializedRef.current = true;
+
+      const initialValue = initialValueRef.current;
+      if (initialValue && initialValue.trim() !== '') {
+        editorInstance.commands.setContent(initialValue, {
+          contentType: 'markdown',
+        });
+        lastSyncedValueRef.current = initialValue;
+      }
+
+      adaptedEditorRef.current = createTipTapAdapter(editorInstance);
+      onEditorReadyRef.current?.(adaptedEditorRef.current);
+
+      if (autoFocusRef.current) {
+        editorInstance.commands.focus();
+      }
+    },
+    [],
+  );
 
   const handleUpdate = useCallback(
     ({ editor: editorInstance }: { editor: TipTapEditor }) => {
-      if (onChange) {
+      if (onChange && !isUpdatingFromPropsRef.current) {
         const markdown = editorInstance.getMarkdown();
         onChange(markdown);
+        lastSyncedValueRef.current = markdown;
       }
     },
     [onChange],
@@ -84,7 +122,7 @@ const RichEditor: React.FC<RichEditorProps> = ({
         placeholder,
       }),
     ],
-    content: value || '',
+    onCreate: handleCreate,
     onUpdate: handleUpdate,
     onFocus: handleFocus,
     onBlur: handleBlur,
@@ -96,67 +134,56 @@ const RichEditor: React.FC<RichEditorProps> = ({
   });
 
   useEffect(() => {
-    if (!editor) {
+    if (
+      !editor ||
+      !isInitializedRef.current ||
+      !isViewAvailable(editor) ||
+      value === lastSyncedValueRef.current
+    ) {
       return;
     }
 
-    const checkEditorReady = () => {
-      if (editor.view && editor.view.dom) {
+    try {
+      const currentMarkdown = editor.getMarkdown();
+      if (currentMarkdown !== value) {
+        isUpdatingFromPropsRef.current = true;
         if (value && value.trim() !== '') {
           editor.commands.setContent(value, { contentType: 'markdown' });
         } else {
           editor.commands.clearContent();
         }
         lastSyncedValueRef.current = value || '';
-        if (!adaptedEditorRef.current) {
-          adaptedEditorRef.current = createTipTapAdapter(editor);
-        }
-        onEditorReady?.(adaptedEditorRef.current);
-      } else {
-        setTimeout(checkEditorReady, 10);
+        setTimeout(() => {
+          isUpdatingFromPropsRef.current = false;
+        }, 0);
       }
-    };
-
-    checkEditorReady();
-  }, [editor]);
-
-  useEffect(() => {
-    if (!editor) {
-      return;
-    }
-
-    if (value === lastSyncedValueRef.current) {
-      return;
-    }
-
-    const currentMarkdown = editor.getMarkdown();
-    if (currentMarkdown !== value) {
-      if (value && value.trim() !== '') {
-        editor.commands.setContent(value, { contentType: 'markdown' });
-      } else {
-        editor.commands.clearContent();
-      }
-      lastSyncedValueRef.current = value || '';
+    } catch (error) {
+      console.warn('Editor view not available when syncing value:', error);
     }
   }, [editor, value]);
 
   useEffect(() => {
-    if (editor && autoFocus) {
-      setTimeout(() => {
-        editor.commands.focus();
-      }, 100);
-    }
-  }, [editor, autoFocus]);
+    initialValueRef.current = value;
+    lastSyncedValueRef.current = value;
+    isInitializedRef.current = false;
+    adaptedEditorRef.current = null;
+    isUpdatingFromPropsRef.current = false;
+
+    return () => {
+      if (editor) {
+        editor.destroy();
+      }
+      isInitializedRef.current = false;
+      adaptedEditorRef.current = null;
+      isUpdatingFromPropsRef.current = false;
+    };
+  }, [editor]);
 
   if (!editor) {
     return <div className="editor-loading">Loading editor...</div>;
   }
 
-  return (
-    <div className="rich-editor-wrap">
-      <EditorContent editor={editor} />
-    </div>
-  );
+  return <EditorContent className="rich-editor-wrap" editor={editor} />;
 };
 
 export default RichEditor;
diff --git a/ui/src/components/Editor/ToolBars/chart.tsx 
b/ui/src/components/Editor/ToolBars/chart.tsx
deleted file mode 100644
index a26a5638..00000000
--- a/ui/src/components/Editor/ToolBars/chart.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { FC, useEffect, useState, memo } from 'react';
-import { Dropdown } from 'react-bootstrap';
-import { useTranslation } from 'react-i18next';
-
-import ToolItem from '../toolItem';
-import { IEditorContext } from '../types';
-
-const Chart: FC<IEditorContext> = ({ editor }) => {
-  const { t } = useTranslation('translation', { keyPrefix: 'editor' });
-
-  const headerList = [
-    {
-      label: t('chart.flow_chart'),
-      tpl: `graph TD
-      A[Christmas] -->|Get money| B(Go shopping)
-      B --> C{Let me think}
-      C -->|One| D[Laptop]
-      C -->|Two| E[iPhone]
-      C -->|Three| F[fa:fa-car Car]`,
-    },
-    {
-      label: t('chart.sequence_diagram'),
-      tpl: `sequenceDiagram
-      Alice->>+John: Hello John, how are you?
-      Alice->>+John: John, can you hear me?
-      John-->>-Alice: Hi Alice, I can hear you!
-      John-->>-Alice: I feel great!
-              `,
-    },
-    {
-      label: t('chart.state_diagram'),
-      tpl: `stateDiagram-v2
-      [*] --> Still
-      Still --> [*]
-      Still --> Moving
-      Moving --> Still
-      Moving --> Crash
-      Crash --> [*]
-              `,
-    },
-    {
-      label: t('chart.class_diagram'),
-      tpl: `classDiagram
-      Animal <|-- Duck
-      Animal <|-- Fish
-      Animal <|-- Zebra
-      Animal : +int age
-      Animal : +String gender
-      Animal: +isMammal()
-      Animal: +mate()
-      class Duck{
-        +String beakColor
-        +swim()
-        +quack()
-      }
-      class Fish{
-        -int sizeInFeet
-        -canEat()
-      }
-      class Zebra{
-        +bool is_wild
-        +run()
-      }
-              `,
-    },
-    {
-      label: t('chart.pie_chart'),
-      tpl: `pie title Pets adopted by volunteers
-      "Dogs" : 386
-      "Cats" : 85
-      "Rats" : 15
-              `,
-    },
-    {
-      label: t('chart.gantt_chart'),
-      tpl: `gantt
-      title A Gantt Diagram
-      dateFormat  YYYY-MM-DD
-      section Section
-      A task           :a1, 2014-01-01, 30d
-      Another task     :after a1  , 20d
-      section Another
-      Task in sec      :2014-01-12  , 12d
-      another task      : 24d
-              `,
-    },
-    {
-      label: t('chart.entity_relationship_diagram'),
-      tpl: `erDiagram
-      CUSTOMER }|..|{ DELIVERY-ADDRESS : has
-      CUSTOMER ||--o{ ORDER : places
-      CUSTOMER ||--o{ INVOICE : "liable for"
-      DELIVERY-ADDRESS ||--o{ ORDER : receives
-      INVOICE ||--|{ ORDER : covers
-      ORDER ||--|{ ORDER-ITEM : includes
-      PRODUCT-CATEGORY ||--|{ PRODUCT : contains
-      PRODUCT ||--o{ ORDER-ITEM : "ordered in"
-        `,
-    },
-  ];
-  const item = {
-    label: 'chart',
-    tip: `${t('chart.text')}`,
-  };
-  const [isShow, setShowState] = useState(false);
-  const [isLocked, setLockState] = useState(false);
-
-  useEffect(() => {
-    if (!editor) {
-      return;
-    }
-    editor.on('focus', () => {
-      setShowState(false);
-    });
-  }, []);
-
-  const click = (tpl) => {
-    const { ch } = editor.getCursor();
-
-    editor.replaceSelection(`${ch ? '\n' : 
''}\`\`\`mermaid\n${tpl}\n\`\`\`\n`);
-  };
-
-  const onAddHeader = () => {
-    setShowState(!isShow);
-  };
-  const handleMouseEnter = () => {
-    if (isLocked) {
-      return;
-    }
-    setLockState(true);
-  };
-
-  const handleMouseLeave = () => {
-    setLockState(false);
-  };
-  return (
-    <ToolItem
-      as="dropdown"
-      {...item}
-      onClick={onAddHeader}
-      onBlur={onAddHeader}>
-      <Dropdown.Menu
-        onMouseEnter={handleMouseEnter}
-        onMouseLeave={handleMouseLeave}>
-        {headerList.map((header) => {
-          return (
-            <Dropdown.Item
-              key={header.label}
-              onClick={(e) => {
-                e.preventDefault();
-                click(header.tpl);
-              }}>
-              {header.label}
-            </Dropdown.Item>
-          );
-        })}
-      </Dropdown.Menu>
-    </ToolItem>
-  );
-};
-
-export default memo(Chart);
diff --git a/ui/src/components/Editor/ToolBars/index.ts 
b/ui/src/components/Editor/ToolBars/index.ts
index 05912bc6..c3fa24be 100644
--- a/ui/src/components/Editor/ToolBars/index.ts
+++ b/ui/src/components/Editor/ToolBars/index.ts
@@ -31,7 +31,6 @@ import Link from './link';
 import BlockQuote from './blockquote';
 import Image from './image';
 import Help from './help';
-import Chart from './chart';
 import File from './file';
 
 export {
@@ -49,6 +48,5 @@ export {
   BlockQuote,
   Image,
   Help,
-  Chart,
   File,
 };
diff --git a/ui/src/components/Editor/index.tsx 
b/ui/src/components/Editor/index.tsx
index c293fa65..eb798c9d 100644
--- a/ui/src/components/Editor/index.tsx
+++ b/ui/src/components/Editor/index.tsx
@@ -98,10 +98,10 @@ const MDEditor: ForwardRefRenderFunction<EditorRef, Props> 
= (
         return;
       }
 
-      setMode(newMode);
       setCurrentEditor(null);
+      setMode(newMode);
     },
-    [mode, currentEditor],
+    [mode],
   );
 
   const getHtml = useCallback(() => {
@@ -172,6 +172,7 @@ const MDEditor: ForwardRefRenderFunction<EditorRef, Props> 
= (
         </div>
 
         <EditorComponent
+          key={mode}
           value={value}
           onChange={(markdown) => {
             onChange?.(markdown);
diff --git a/ui/src/components/Editor/types.ts 
b/ui/src/components/Editor/types.ts
index 926d3c20..e7d23081 100644
--- a/ui/src/components/Editor/types.ts
+++ b/ui/src/components/Editor/types.ts
@@ -116,11 +116,12 @@ export interface CodeMirrorEditor extends Editor {
   moduleType;
 }
 
-// @deprecated 已废弃,请直接使用 Editor 接口
-// 保留此接口仅用于向后兼容,新代码不应使用
-export interface IEditorContext {
-  editor: Editor;
-  wrapText?;
-  replaceLines?;
-  appendBlock?;
+export interface BaseEditorProps {
+  value: string;
+  onChange?: (value: string) => void;
+  onFocus?: () => void;
+  onBlur?: () => void;
+  placeholder?: string;
+  autoFocus?: boolean;
+  onEditorReady?: (editor: Editor) => void;
 }
diff --git a/ui/src/components/Editor/utils/codemirror/commands.ts 
b/ui/src/components/Editor/utils/codemirror/commands.ts
index 152bc632..546382d4 100644
--- a/ui/src/components/Editor/utils/codemirror/commands.ts
+++ b/ui/src/components/Editor/utils/codemirror/commands.ts
@@ -69,14 +69,26 @@ export function createCommandMethods(editor: Editor) {
 
       const newLines = lines.map(replace) as string[];
       const newText = newLines.join('\n');
-      editor.setValue(newText);
+      editor.dispatch({
+        changes: {
+          from: 0,
+          to: editor.state.doc.length,
+          insert: newText,
+        },
+      });
     },
 
     appendBlock: (content: string) => {
       const { doc } = editor.state;
       const currentText = doc.toString();
       const newText = currentText ? `${currentText}\n\n${content}` : content;
-      editor.setValue(newText);
+      editor.dispatch({
+        changes: {
+          from: 0,
+          to: editor.state.doc.length,
+          insert: newText,
+        },
+      });
     },
 
     insertBold: (text?: string) => {
diff --git a/ui/src/components/Editor/utils/index.ts 
b/ui/src/components/Editor/utils/index.ts
index 938b3761..5ba7192c 100644
--- a/ui/src/components/Editor/utils/index.ts
+++ b/ui/src/components/Editor/utils/index.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef } from 'react';
 
 import { minimalSetup } from 'codemirror';
 import { EditorState, Compartment } from '@codemirror/state';
@@ -129,12 +129,14 @@ export const useEditor = ({
   editorRef,
   placeholder: placeholderText,
   autoFocus,
+  initialValue,
   onChange,
   onFocus,
   onBlur,
 }) => {
   const [editor, setEditor] = useState<Editor | null>(null);
-  const [value, setValue] = useState<string>('');
+  const isInternalUpdateRef = useRef<boolean>(false);
+
   const init = async () => {
     const isDark = isDarkTheme();
 
@@ -162,6 +164,7 @@ export const useEditor = ({
     });
 
     const startState = EditorState.create({
+      doc: initialValue || '',
       extensions: [
         minimalSetup,
         markdown({
@@ -206,9 +209,20 @@ export const useEditor = ({
       }, 10);
     }
 
+    const originalSetValue = cm.setValue;
+    cm.setValue = (newValue: string) => {
+      isInternalUpdateRef.current = true;
+      originalSetValue.call(cm, newValue);
+      setTimeout(() => {
+        isInternalUpdateRef.current = false;
+      }, 0);
+    };
+
     cm.on('change', () => {
-      const newValue = cm.getValue();
-      setValue(newValue);
+      if (!isInternalUpdateRef.current && onChange) {
+        const newValue = cm.getValue();
+        onChange(newValue);
+      }
     });
 
     cm.on('focus', () => {
@@ -224,10 +238,6 @@ export const useEditor = ({
     return cm;
   };
 
-  useEffect(() => {
-    onChange?.(value);
-  }, [value]);
-
   useEffect(() => {
     if (!editorRef.current) {
       return;
diff --git a/ui/src/components/Editor/utils/tiptap/events.ts 
b/ui/src/components/Editor/utils/tiptap/events.ts
index 495ee6e5..2a86440d 100644
--- a/ui/src/components/Editor/utils/tiptap/events.ts
+++ b/ui/src/components/Editor/utils/tiptap/events.ts
@@ -25,7 +25,13 @@ import { logWarning } from './errorHandler';
  * Checks if editor view is available
  */
 function isViewAvailable(editor: TipTapEditor): boolean {
-  return !!(editor.view && editor.view.dom);
+  if (!editor) {
+    return false;
+  }
+  if (editor.isDestroyed) {
+    return false;
+  }
+  return !!(editor.view && editor.state);
 }
 
 /**

Reply via email to