diff --git a/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx b/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx index b6657450..0ee795ba 100644 --- a/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx +++ b/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx @@ -11,13 +11,13 @@ 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 { type Suggestion } from './plugin/AutocompletePlugin'; import CharacterCountPlugin from './plugin/CharacterCountPlugin'; -import InitialValuePlugin from './plugin/InitialValuePlugin'; -import CommandPlugin from './plugin/CommandPlugin'; +import Jinja2InitialValuePlugin from './plugin/Jinja2InitialValuePlugin'; +import Jinja2AutocompletePlugin from './plugin/Jinja2AutocompletePlugin'; import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin'; +import Jinja2BlurPlugin from './plugin/Jinja2BlurPlugin'; import LineNumberPlugin from './plugin/LineNumberPlugin'; -import BlurPlugin from './plugin/BlurPlugin'; const jinja2Theme = { paragraph: 'editor-paragraph', @@ -171,13 +171,12 @@ const Jinja2Editor: FC = ({ ErrorBoundary={LexicalErrorBoundary} /> - - - {}} onChange={onChange} /> - - + + {}} onChange={onChange} waitForInit /> + + ); diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index 8e72ca72..20674156 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -33,6 +33,7 @@ export interface LexicalEditorProps { type?: 'input' | 'textarea'; language?: 'string' | 'jinja2'; className?: string; + waitForInit?: boolean; } // Default theme for editor @@ -55,8 +56,10 @@ const Editor: FC =({ type = 'textarea', language = 'string', height, - className + className, + waitForInit = false, }) => { + console.log('Editor value', value) const [_count, setCount] = useState(0); if (language === 'jinja2') { @@ -145,10 +148,10 @@ const Editor: FC =({ /> - - { setCount(count) }} onChange={onChange} /> - - + + { setCount(count) }} onChange={onChange} waitForInit={waitForInit || !!value} /> + + ); diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index 863e5160..92d0f2e4 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -2,11 +2,11 @@ * @Author: ZhaoYing * @Date: 2025-12-23 16:22:51 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-25 16:13:37 + * @Last Modified time: 2026-04-02 17:12:41 */ import { useEffect, useState, useRef, type FC } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical'; +import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical'; import { Space, Flex } from 'antd'; import { INSERT_VARIABLE_COMMAND, CLOSE_AUTOCOMPLETE_COMMAND } from '../commands'; @@ -26,7 +26,7 @@ export interface Suggestion { } // Autocomplete plugin for variable suggestions triggered by '/' character -const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => { +const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => { const [editor] = useLexicalComposerContext(); const [showSuggestions, setShowSuggestions] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); @@ -129,34 +129,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> // Insert selected suggestion into editor const insertMention = (suggestion: Suggestion) => { - if (enableJinja2) { - // In Jinja2 mode, insert {{variable}} format text - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - const anchorNode = selection.anchor.getNode(); - const anchorOffset = selection.anchor.offset; - const nodeText = anchorNode.getTextContent(); - - // Remove trigger character '/' - const textBefore = nodeText.substring(0, anchorOffset - 1); - const textAfter = nodeText.substring(anchorOffset); - const newText = textBefore + `{{${suggestion.value}}}` + textAfter; - - if ($isTextNode(anchorNode)) { - anchorNode.setTextContent(newText); - } - - // Set cursor position after inserted text - const newOffset = textBefore.length + `{{${suggestion.value}}}`.length; - selection.anchor.offset = newOffset; - selection.focus.offset = newOffset; - } - }); - } else { - // In normal mode, use VariableNode - editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion }); - } + editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion }); setShowSuggestions(false); }; diff --git a/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx index 13eb48b6..30e437d4 100644 --- a/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx @@ -1,64 +1,33 @@ /* * @Author: ZhaoYing * @Date: 2026-01-20 10:42:13 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-03 10:12:10 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-02 17:13:08 */ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useEffect } from 'react'; -import { $setSelection } from 'lexical'; import { CLOSE_AUTOCOMPLETE_COMMAND } from '../commands'; // Plugin to handle blur events and close autocomplete when clicking outside -export default function BlurPlugin({ enableJinja2 }: { enableJinja2: boolean }) { +export default function BlurPlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => { - // Close autocomplete when clicking outside the popup const handleClickOutside = (e: MouseEvent) => { - const target = e.target as HTMLElement; - if (target?.closest('[data-autocomplete-popup="true"]')) { - return; - } + if ((e.target as HTMLElement)?.closest('[data-autocomplete-popup="true"]')) return; editor.dispatchCommand(CLOSE_AUTOCOMPLETE_COMMAND, undefined); }; - document.addEventListener('mousedown', handleClickOutside); return editor.registerRootListener((rootElement) => { if (rootElement) { - const handleBlur = (e: FocusEvent) => { - if (enableJinja2) { - // Check if autocomplete popup was clicked - const target = e.target as HTMLElement; - if (target?.closest('[data-autocomplete-popup="true"]')) { - return; - } - - // Check if blur was caused by paste operation - const relatedTarget = e.relatedTarget as HTMLElement; - if (!relatedTarget || relatedTarget === document.body) { - return; - } - - // Clear selection on blur - editor.update(() => { - $setSelection(null); - }); - } - }; - - rootElement.addEventListener('blur', handleBlur); return () => { document.removeEventListener('mousedown', handleClickOutside); - rootElement.removeEventListener('blur', handleBlur); }; } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; + return () => { document.removeEventListener('mousedown', handleClickOutside); }; }); - }, [editor, enableJinja2]); + }, [editor]); return null; } diff --git a/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx index 4579375a..322f5ccf 100644 --- a/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx @@ -1,49 +1,73 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-23 16:22:51 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-02 17:13:45 + */ import { useEffect, useRef } from 'react'; import { $getRoot, $isParagraphNode } from 'lexical'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $isVariableNode } from '../nodes/VariableNode'; -const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => { +const serialize = (root: ReturnType): string => { + const paragraphs: string[] = []; + root.getChildren().forEach(child => { + if ($isParagraphNode(child)) { + let content = ''; + child.getChildren().forEach(node => { + content += $isVariableNode(node) ? node.getTextContent() : node.getTextContent(); + }); + paragraphs.push(content); + } + }); + return paragraphs.join('\n'); +}; + +const CharacterCountPlugin = ({ + setCount, + onChange, + waitForInit = false, +}: { + setCount: (count: number) => void; + onChange?: (value: string) => void; + waitForInit?: boolean; +}) => { const [editor] = useLexicalComposerContext(); - const isReadyRef = useRef(false); + // lastProgrammaticValue tracks what InitialValuePlugin wrote, so we can + // suppress onChange when the content hasn't actually changed from that value. + const lastProgrammaticValueRef = useRef(null); + const isReadyRef = useRef(!waitForInit); + const isFirstUpdateRef = useRef(true); useEffect(() => { return editor.registerUpdateListener(({ editorState, tags }) => { if (tags.has('programmatic')) { isReadyRef.current = true; + isFirstUpdateRef.current = false; + editorState.read(() => { + lastProgrammaticValueRef.current = serialize($getRoot()); + }); return; } if (!isReadyRef.current) return; editorState.read(() => { - const root = $getRoot(); - let serializedContent = ''; - - // Traverse all nodes and serialize properly - const paragraphs: string[] = []; - root.getChildren().forEach(child => { - if ($isParagraphNode(child)) { - let paragraphContent = ''; - child.getChildren().forEach(node => { - if ($isVariableNode(node)) { - paragraphContent += node.getTextContent(); - } else { - paragraphContent += node.getTextContent(); - } - }); - paragraphs.push(paragraphContent); - } - }); - - serializedContent = paragraphs.join('\n'); - - setCount(serializedContent.length); - onChange?.(serializedContent); + const content = serialize($getRoot()); + // Skip the first update if content is empty (editor initial render) + if (isFirstUpdateRef.current) { + isFirstUpdateRef.current = false; + if (content === '') return; + } + // Skip if content is identical to what was programmatically written + if (content === lastProgrammaticValueRef.current) return; + lastProgrammaticValueRef.current = null; + setCount(content.length); + onChange?.(content); }); }); }, [editor, setCount, onChange]); return null; -} +}; -export default CharacterCountPlugin \ No newline at end of file +export default CharacterCountPlugin; diff --git a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx index 0ebcbe77..4186e80c 100644 --- a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx @@ -1,3 +1,9 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-23 16:22:51 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-02 17:14:15 + */ import { useEffect, useRef } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical'; @@ -8,19 +14,17 @@ import { type Suggestion } from '../plugin/AutocompletePlugin' interface InitialValuePluginProps { value: string; options?: Suggestion[]; - enableLineNumbers?: boolean; } -const InitialValuePlugin: React.FC = ({ value, options = [], enableLineNumbers = false }) => { +const InitialValuePlugin: React.FC = ({ value, options = [] }) => { const [editor] = useLexicalComposerContext(); const prevValueRef = useRef(''); - const prevEnableLineNumbersRef = useRef(enableLineNumbers); const isUserInputRef = useRef(false); const optionsRef = useRef(options); optionsRef.current = options; useEffect(() => { - const removeListener = editor.registerUpdateListener(({ editorState, tags }) => { + return editor.registerUpdateListener(({ editorState, tags }) => { if (tags.has('programmatic')) return; editorState.read(() => { const root = $getRoot(); @@ -31,21 +35,16 @@ const InitialValuePlugin: React.FC = ({ value, options } }); }); - - return removeListener; }, [editor]); useEffect(() => { - if (value !== prevValueRef.current || enableLineNumbers !== prevEnableLineNumbersRef.current) { - // Skip reset if the change was triggered by user input (avoid cursor jump) - if (isUserInputRef.current && enableLineNumbers === prevEnableLineNumbersRef.current) { + if (value !== prevValueRef.current) { + if (isUserInputRef.current) { prevValueRef.current = value; isUserInputRef.current = false; return; } - // Update refs BEFORE editor.update to prevent re-entry prevValueRef.current = value; - prevEnableLineNumbersRef.current = enableLineNumbers; isUserInputRef.current = false; queueMicrotask(() => { @@ -54,16 +53,7 @@ const InitialValuePlugin: React.FC = ({ value, options root.clear(); const parts = value.split(/(\{\{[^}]+\}\}|\n)/); - - if (enableLineNumbers) { - const lines = value.split('\n'); - lines.forEach((line) => { - const paragraph = $createParagraphNode(); - paragraph.append($createTextNode(line)); - root.append(paragraph); - }); - } else { - let paragraph = $createParagraphNode(); + let paragraph = $createParagraphNode(); parts.forEach(part => { if (part === '\n') { @@ -118,15 +108,10 @@ const InitialValuePlugin: React.FC = ({ value, options } }); root.append(paragraph); - } }, { tag: 'programmatic' }); }); - } else { - prevValueRef.current = value; - prevEnableLineNumbersRef.current = enableLineNumbers; - isUserInputRef.current = false; } - }, [value, editor, enableLineNumbers]); + }, [value, editor]); return null; }; diff --git a/web/src/views/Workflow/components/Editor/plugin/Jinja2AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Jinja2AutocompletePlugin.tsx new file mode 100644 index 00000000..00b4c3d1 --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/Jinja2AutocompletePlugin.tsx @@ -0,0 +1,199 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-02 17:10:59 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-02 17:10:59 + */ +import { useEffect, useState, useRef, type FC } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { + $getSelection, $isRangeSelection, $isTextNode, + COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, + KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND, +} from 'lexical'; +import { Space, Flex } from 'antd'; + +import { CLOSE_AUTOCOMPLETE_COMMAND } from '../commands'; +import type { Suggestion } from './AutocompletePlugin'; + +const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => { + const [editor] = useLexicalComposerContext(); + const [showSuggestions, setShowSuggestions] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(0); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + const popupRef = useRef(null); + + const scrollSelectedIntoView = () => { + if (!popupRef.current) return; + const selectedElement = popupRef.current.querySelector('[data-selected="true"]'); + if (!selectedElement) return; + const container = popupRef.current; + const element = selectedElement as HTMLElement; + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + if (elementRect.bottom > containerRect.bottom) { + container.scrollTop += elementRect.bottom - containerRect.bottom; + } else if (elementRect.top < containerRect.top) { + container.scrollTop -= containerRect.top - elementRect.top; + } + }; + + useEffect(() => { + return editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const selection = $getSelection(); + if (!selection || !$isRangeSelection(selection)) { + setShowSuggestions(false); + return; + } + const anchorNode = selection.anchor.getNode(); + const anchorOffset = selection.anchor.offset; + const textBeforeCursor = anchorNode.getTextContent().substring(0, anchorOffset); + const shouldShow = textBeforeCursor.endsWith('/'); + setShowSuggestions(shouldShow); + if (!shouldShow) { setSelectedIndex(0); return; } + + const domSelection = window.getSelection(); + if (domSelection && domSelection.rangeCount > 0) { + const rect = domSelection.getRangeAt(0).getBoundingClientRect(); + const popupWidth = 280, popupHeight = 200; + const vw = window.innerWidth, vh = window.innerHeight; + let left = Math.min(Math.max(rect.left, 10), vw - popupWidth - 10); + let top = rect.top - 10; + if (top - popupHeight < 10) { + top = Math.min(rect.bottom + 10, vh - popupHeight - 10); + } + setPopupPosition({ top, left }); + } + }); + }); + }, [editor]); + + useEffect(() => { + return editor.registerCommand( + CLOSE_AUTOCOMPLETE_COMMAND, + () => { setShowSuggestions(false); return true; }, + COMMAND_PRIORITY_HIGH, + ); + }, [editor]); + + const insertMention = (suggestion: Suggestion) => { + editor.update(() => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) return; + const anchorNode = selection.anchor.getNode(); + const anchorOffset = selection.anchor.offset; + const nodeText = anchorNode.getTextContent(); + const textBefore = nodeText.substring(0, anchorOffset - 1); + const textAfter = nodeText.substring(anchorOffset); + const inserted = `{{${suggestion.value}}}`; + if ($isTextNode(anchorNode)) { + anchorNode.setTextContent(textBefore + inserted + textAfter); + const newOffset = textBefore.length + inserted.length; + selection.anchor.offset = newOffset; + selection.focus.offset = newOffset; + } + }); + setShowSuggestions(false); + }; + + const groupedSuggestions = options.reduce((groups: Record, s) => { + const id = s.nodeData.id as string; + if (!groups[id]) groups[id] = []; + groups[id].push(s); + return groups; + }, {}); + + const allOptions = Object.values(groupedSuggestions).flat(); + + useEffect(() => { + if (!showSuggestions) return; + return editor.registerCommand( + KEY_ENTER_COMMAND, + (event) => { + const opt = allOptions[selectedIndex]; + if (opt && !opt.disabled) { event?.preventDefault(); insertMention(opt); return true; } + return false; + }, + COMMAND_PRIORITY_HIGH, + ); + }, [showSuggestions, selectedIndex, allOptions]); + + useEffect(() => { + if (!showSuggestions) return; + const down = editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (e) => { + e?.preventDefault(); + setSelectedIndex(prev => { + let next = prev + 1; + while (next < allOptions.length && allOptions[next].disabled) next++; + setTimeout(scrollSelectedIntoView, 0); + return next >= allOptions.length ? prev : next; + }); + return true; + }, COMMAND_PRIORITY_HIGH); + const up = editor.registerCommand(KEY_ARROW_UP_COMMAND, (e) => { + e?.preventDefault(); + setSelectedIndex(prev => { + let p = prev - 1; + while (p >= 0 && allOptions[p].disabled) p--; + setTimeout(scrollSelectedIntoView, 0); + return p < 0 ? prev : p; + }); + return true; + }, COMMAND_PRIORITY_HIGH); + const esc = editor.registerCommand(KEY_ESCAPE_COMMAND, (e) => { + e?.preventDefault(); setShowSuggestions(false); return true; + }, COMMAND_PRIORITY_HIGH); + return () => { down(); up(); esc(); }; + }, [showSuggestions, selectedIndex, allOptions, editor]); + + if (!showSuggestions || Object.keys(groupedSuggestions).length === 0) return null; + + return ( +
e.preventDefault()} + className="rb:fixed rb:z-1000 rb:py-1 rb:bg-white rb:rounded-xl rb:min-w-70 rb:max-h-50 rb:overflow-y-auto rb:transform-[translateY(-100%)] rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]" + style={{ top: popupPosition.top, left: popupPosition.left }} + > + + {Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => ( +
+ + {nodeOptions[0]?.nodeData?.icon && } + {nodeOptions[0]?.nodeData?.name || nodeId} + + {nodeOptions.map((option) => { + const globalIndex = allOptions.indexOf(option); + return ( + !option.disabled && insertMention(option)} + onMouseEnter={() => setSelectedIndex(globalIndex)} + > + + {option.isContext ? '📄' : '{x}'} + {option.label} + + {option.dataType && {option.dataType}} + + ); + })} +
+ ))} +
+
+ ); +}; + +export default Jinja2AutocompletePlugin; diff --git a/web/src/views/Workflow/components/Editor/plugin/Jinja2BlurPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Jinja2BlurPlugin.tsx new file mode 100644 index 00000000..177baf30 --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/Jinja2BlurPlugin.tsx @@ -0,0 +1,41 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-02 17:11:04 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-02 17:11:04 + */ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { useEffect } from 'react'; +import { $setSelection } from 'lexical'; +import { CLOSE_AUTOCOMPLETE_COMMAND } from '../commands'; + +export default function Jinja2BlurPlugin() { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ((e.target as HTMLElement)?.closest('[data-autocomplete-popup="true"]')) return; + editor.dispatchCommand(CLOSE_AUTOCOMPLETE_COMMAND, undefined); + }; + document.addEventListener('mousedown', handleClickOutside); + + return editor.registerRootListener((rootElement) => { + if (rootElement) { + const handleBlur = (e: FocusEvent) => { + if ((e.target as HTMLElement)?.closest('[data-autocomplete-popup="true"]')) return; + const relatedTarget = e.relatedTarget as HTMLElement; + if (!relatedTarget || relatedTarget === document.body) return; + editor.update(() => { $setSelection(null); }); + }; + rootElement.addEventListener('blur', handleBlur); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + rootElement.removeEventListener('blur', handleBlur); + }; + } + return () => { document.removeEventListener('mousedown', handleClickOutside); }; + }); + }, [editor]); + + return null; +} diff --git a/web/src/views/Workflow/components/Editor/plugin/Jinja2InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Jinja2InitialValuePlugin.tsx new file mode 100644 index 00000000..6b5f7363 --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/Jinja2InitialValuePlugin.tsx @@ -0,0 +1,61 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-02 17:11:07 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-02 17:11:07 + */ +import { useEffect, useRef } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical'; + +interface Jinja2InitialValuePluginProps { + value: string; +} + +const Jinja2InitialValuePlugin: React.FC = ({ value }) => { + const [editor] = useLexicalComposerContext(); + const prevValueRef = useRef(''); + const isUserInputRef = useRef(false); + + useEffect(() => { + return editor.registerUpdateListener(({ editorState, tags }) => { + if (tags.has('programmatic')) return; + editorState.read(() => { + const textContent = $getRoot().getTextContent(); + if (textContent !== prevValueRef.current) { + isUserInputRef.current = true; + prevValueRef.current = textContent; + } + }); + }); + }, [editor]); + + useEffect(() => { + if (value === prevValueRef.current) return; + + if (isUserInputRef.current) { + prevValueRef.current = value; + isUserInputRef.current = false; + return; + } + + prevValueRef.current = value; + isUserInputRef.current = false; + + queueMicrotask(() => { + editor.update(() => { + const root = $getRoot(); + root.clear(); + value.split('\n').forEach((line) => { + const paragraph = $createParagraphNode(); + paragraph.append($createTextNode(line)); + root.append(paragraph); + }); + }, { tag: 'programmatic' }); + }); + }, [value, editor]); + + return null; +}; + +export default Jinja2InitialValuePlugin; diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx index a3ed79e8..53714327 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-09 18:35:43 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-20 11:32:44 + * @Last Modified time: 2026-04-02 17:17:06 */ import { type FC, useRef, useState } from "react"; import { useTranslation } from 'react-i18next' @@ -114,6 +114,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} variant="outlined" type="input" @@ -212,13 +213,15 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an } {values?.body?.content_type === 'binary' && vo.dataType.includes('file'))} type="input" size="small" + height={28} /> }