diff --git a/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx b/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx new file mode 100644 index 00000000..b6657450 --- /dev/null +++ b/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx @@ -0,0 +1,186 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-02 15:15:36 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-02 15:15:36 + */ +import { type FC, useEffect, useMemo } from 'react'; +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; + +import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin'; +import CharacterCountPlugin from './plugin/CharacterCountPlugin'; +import InitialValuePlugin from './plugin/InitialValuePlugin'; +import CommandPlugin from './plugin/CommandPlugin'; +import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin'; +import LineNumberPlugin from './plugin/LineNumberPlugin'; +import BlurPlugin from './plugin/BlurPlugin'; + +const jinja2Theme = { + paragraph: 'editor-paragraph', + code: 'jinja2-expression', + text: { + bold: 'editor-text-bold', + italic: 'editor-text-italic', + code: 'jinja2-inline', + }, +}; + +const initialConfig = { + namespace: 'AutocompleteEditor', + theme: jinja2Theme, + nodes: [], + onError: (error: Error) => console.error(error), +}; + +const STYLE_ID = 'code-editor-styles'; +const JINJA2_STYLES = ` + .jinja2-expression { + background-color: #f6f8fa !important; + border: 1px solid #d1d9e0 !important; + border-radius: 3px !important; + padding: 2px 4px !important; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; + font-size: 13px !important; + color: #0969da !important; + } + .jinja2-inline { + background-color: #f6f8fa !important; + padding: 1px 3px !important; + border-radius: 2px !important; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; + font-size: 13px !important; + color: #0969da !important; + } + .editor-paragraph { margin: 0; } + .editor-with-line-numbers { display: flex; } + .line-numbers { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 12px; + line-height: 16px; + padding: 4px 8px; + text-align: right; + user-select: none; + display: flex; + flex-direction: column; + } + .line-numbers > div { min-height: 20px; display: flex; align-items: flex-start; } + .editor-content-wrapper { flex: 1; } + .editor-content-with-numbers { + white-space: pre-wrap; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + } + .editor-content-with-numbers p { margin: 0; min-height: 20px; } +`; + +export interface Jinja2EditorProps { + placeholder?: string; + value?: string; + onChange?: (value: string) => void; + options?: Suggestion[]; + variant?: 'outlined' | 'borderless'; + height?: number; + size?: 'default' | 'small'; + className?: string; +} + +const Jinja2Editor: FC = ({ + placeholder = '请输入内容...', + value = '', + onChange, + options = [], + variant = 'borderless', + size = 'default', + height, + className, +}) => { + useEffect(() => { + if (!document.getElementById(STYLE_ID)) { + const style = document.createElement('style'); + style.id = STYLE_ID; + style.textContent = JINJA2_STYLES; + document.head.appendChild(style); + } + }, []); + + const minheight = useMemo( + () => `${height ?? (size === 'small' ? 60 : 120)}px`, + [height, size], + ); + + const fontSize = size === 'small' ? '12px' : '14px'; + + const lineHeight = useMemo( + () => `${height ? height - 10 : size === 'small' ? 16 : 20}px`, + [height, size], + ); + + const placeHolderMinheight = `${height ? 16 : size === 'small' ? 16 : 30}px`; + + return ( + +
+ +
+
1
+
+
+ +
+
+ } + placeholder={ +
+ {placeholder} +
+ } + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + {}} onChange={onChange} /> + + + +
+ ); +}; + +export default Jinja2Editor; diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index 60503065..8e72ca72 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -4,26 +4,20 @@ * @Last Modified by: ZhaoYing * @Last Modified time: 2026-03-25 10:58:47 */ -import { type FC, useState, useEffect, useMemo } from 'react'; +import { type FC, useState, useMemo } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; -// import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; -// import { HeadingNode, QuoteNode } from '@lexical/rich-text'; -// import { ListItemNode, ListNode } from '@lexical/list'; -// import { LinkNode } from '@lexical/link'; -// import { CodeNode } from '@lexical/code'; import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin' import CharacterCountPlugin from './plugin/CharacterCountPlugin' import InitialValuePlugin from './plugin/InitialValuePlugin'; import CommandPlugin from './plugin/CommandPlugin'; -import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin'; -import LineNumberPlugin from './plugin/LineNumberPlugin'; import BlurPlugin from './plugin/BlurPlugin'; import { VariableNode } from './nodes/VariableNode' +import Jinja2Editor from './Jinja2Editor'; // Props interface for Lexical Editor component export interface LexicalEditorProps { @@ -50,16 +44,6 @@ const theme = { }, }; -// Theme with Jinja2 syntax highlighting -const jinja2Theme = { - ...theme, - code: 'jinja2-expression', - text: { - ...theme.text, - code: 'jinja2-inline', - }, -}; - // Main Lexical Editor component const Editor: FC =({ placeholder = "请输入内容...", @@ -74,97 +58,27 @@ const Editor: FC =({ className }) => { const [_count, setCount] = useState(0); - const [enableJinja2, setEnableJinja2] = useState(false) - const [enableLineNumbers, setEnableLineNumbers] = useState(false) - // Setup Jinja2 mode and inject styles when language changes - useEffect(() => { - const needsLineNumbers = language === 'jinja2'; - setEnableJinja2(language === 'jinja2'); - setEnableLineNumbers(needsLineNumbers); - - if (needsLineNumbers) { - const styleId = 'code-editor-styles'; - let existingStyle = document.getElementById(styleId); - - if (!existingStyle) { - const style = document.createElement('style'); - style.id = styleId; - style.textContent = ` - .jinja2-expression { - background-color: #f6f8fa !important; - border: 1px solid #d1d9e0 !important; - border-radius: 3px !important; - padding: 2px 4px !important; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; - font-size: 13px !important; - color: #0969da !important; - } - .jinja2-inline { - background-color: #f6f8fa !important; - padding: 1px 3px !important; - border-radius: 2px !important; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; - font-size: 13px !important; - color: #0969da !important; - } - .editor-paragraph { - margin: 0; - } - .editor-paragraph:has-text('{') .editor-text, - .editor-paragraph:has-text('[') .editor-text { - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; - } - .editor-with-line-numbers { - display: flex; - } - .line-numbers { - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; - font-size: 12px; - line-height: 16px; - padding: 4px 8px; - text-align: right; - user-select: none; - display: flex; - flex-direction: column; - } - .line-numbers > div { - min-height: 20px; - display: flex; - align-items: flex-start; - } - .editor-content-wrapper { - flex: 1; - } - .editor-content-with-numbers { - white-space: pre-wrap; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; - } - .editor-content-with-numbers p { - margin: 0; - min-height: 20px; - } - `; - document.head.appendChild(style); - } - } - }, [language]) + if (language === 'jinja2') { + return ( + + ); + } // Lexical editor configuration const initialConfig = { namespace: 'AutocompleteEditor', - theme: enableJinja2 ? jinja2Theme : theme, - nodes: enableJinja2 ? [ - // When Jinja2 is enabled, use plain text instead of VariableNode - ] : [ - // HeadingNode, - // QuoteNode, - // ListItemNode, - // ListNode, - // LinkNode, - // CodeNode, - VariableNode, - ], + theme, + nodes: [VariableNode], onError: (error: Error) => { console.error(error); }, @@ -198,54 +112,26 @@ const Editor: FC =({
-
-
1
-
-
- -
-
- ) : ( - // Standard editor without line numbers - - ) + padding: height ? '4px 6px' : variant === 'borderless' ? '0' : '6px 8px', + border: variant === 'borderless' ? 'none' : '1px solid #EBEBEB', + borderRadius: '8px', + outline: 'none', + resize: 'none', + fontSize: fontSize, + lineHeight: lineHeight, + }} + /> } placeholder={
=({ } ErrorBoundary={LexicalErrorBoundary} /> - {/* Editor plugins */} - {language === 'jinja2' && } - {enableLineNumbers && } - + { setCount(count) }} onChange={onChange} /> - - + +
); diff --git a/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx index ed07392d..4579375a 100644 --- a/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { $getRoot, $isParagraphNode } from 'lexical'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; @@ -6,9 +6,15 @@ import { $isVariableNode } from '../nodes/VariableNode'; const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => { const [editor] = useLexicalComposerContext(); + const isReadyRef = useRef(false); useEffect(() => { - return editor.registerUpdateListener(({ editorState }) => { + return editor.registerUpdateListener(({ editorState, tags }) => { + if (tags.has('programmatic')) { + isReadyRef.current = true; + return; + } + if (!isReadyRef.current) return; editorState.read(() => { const root = $getRoot(); let serializedContent = '';