diff --git a/web/src/views/Workflow/components/Editor/plugin/Jinja2HighlightPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Jinja2HighlightPlugin.tsx index 4c75fc58..fb6212bd 100644 --- a/web/src/views/Workflow/components/Editor/plugin/Jinja2HighlightPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/Jinja2HighlightPlugin.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { TextNode, $createTextNode } from 'lexical'; +import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical'; const Jinja2HighlightPlugin = () => { const [editor] = useLexicalComposerContext(); @@ -18,6 +18,16 @@ const Jinja2HighlightPlugin = () => { const parent = textNode.getParent(); if (!parent) return; + // Preserve selection + const selection = $getSelection(); + let selectionOffset = null; + if ($isRangeSelection(selection)) { + const anchor = selection.anchor; + if (anchor.getNode() === textNode) { + selectionOffset = anchor.offset; + } + } + const tokens = tokenizeJinja2(text); // Skip if no meaningful tokenization (only one text token) @@ -85,6 +95,19 @@ const Jinja2HighlightPlugin = () => { for (let i = 1; i < newNodes.length; i++) { newNodes[i - 1].insertAfter(newNodes[i]); } + + // Restore selection + if (selectionOffset !== null && $isRangeSelection(selection)) { + let currentOffset = 0; + for (const node of newNodes) { + const nodeLength = node.getTextContent().length; + if (currentOffset + nodeLength >= selectionOffset) { + node.select(selectionOffset - currentOffset, selectionOffset - currentOffset); + break; + } + currentOffset += nodeLength; + } + } } }); }, [editor]); diff --git a/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx b/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx index a2c9da37..d1a392ae 100644 --- a/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx +++ b/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx @@ -31,13 +31,11 @@ const JinjaRender: FC = ({ selectedNode, options, templateOpti const form = Form.useFormInstance() const values = Form.useWatch([], form) || {} - console.log('JinjaRender values', values) - const prevMappingNamesRef = useRef([]) const prevTemplateVarsRef = useRef([]) - const syncTimeoutRef = useRef(null) const isSyncingRef = useRef(false) const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null) + const editorKeyRef = useRef(0) // Reset refs when node changes useEffect(() => { @@ -68,46 +66,39 @@ const JinjaRender: FC = ({ selectedNode, options, templateOpti if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return - if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current) - const activeElement = document.activeElement as HTMLElement + let updatedTemplate = String(form.getFieldValue('template') || '') - syncTimeoutRef.current = setTimeout(() => { - let updatedTemplate = String(form.getFieldValue('template') || '') - - prevNames.forEach((oldName, index) => { - const newName = currentMappingNames[index] - if (newName && oldName !== newName) { - updatedTemplate = updatedTemplate.replace( - new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'), - `{{${newName}}}` - ) - } - }) - - if (updatedTemplate !== form.getFieldValue('template')) { - isSyncingRef.current = true - lastSyncSourceRef.current = 'mapping' - - prevTemplateVarsRef.current = extractTemplateVars(updatedTemplate) - prevMappingNamesRef.current = currentMappingNames - form.setFieldValue('template', updatedTemplate) - - requestAnimationFrame(() => { - activeElement?.focus?.() - setTimeout(() => { - isSyncingRef.current = false - lastSyncSourceRef.current = null - }, 50) - }) - } else { - prevMappingNamesRef.current = currentMappingNames + prevNames.forEach((oldName, index) => { + const newName = currentMappingNames[index] + if (newName && oldName !== newName) { + updatedTemplate = updatedTemplate.replace( + new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'), + `{{${newName}}}` + ) } - }, 0) + }) + + + if (updatedTemplate !== form.getFieldValue('template')) { + isSyncingRef.current = true + lastSyncSourceRef.current = 'mapping' + + prevTemplateVarsRef.current = extractTemplateVars(updatedTemplate) + prevMappingNamesRef.current = currentMappingNames + form.setFieldValue('template', updatedTemplate) + editorKeyRef.current++ + + setTimeout(() => { + isSyncingRef.current = false + lastSyncSourceRef.current = null + }, 0) + } else { + prevMappingNamesRef.current = currentMappingNames + } }, [values?.mapping, selectedNode?.data?.type, form]) // Sync mapping when template variables change useEffect(() => { - console.log('values?.template', values?.template) if ( isSyncingRef.current || lastSyncSourceRef.current === 'template' || @@ -155,11 +146,10 @@ const JinjaRender: FC = ({ selectedNode, options, templateOpti } }) - // Remove unused mappings and duplicates + // Remove duplicates only const seenNames = new Set() const finalMapping = updatedMapping.filter(item => { - const isUsed = templateVars.some(v => item.name === v || item.value === `{{${v}}}`) - if (!isUsed || !item.name || seenNames.has(item.name)) return false + if (!item.name || seenNames.has(item.name)) return false seenNames.add(item.name) return true }) @@ -190,6 +180,7 @@ const JinjaRender: FC = ({ selectedNode, options, templateOpti