Merge pull request #822 from SuanmoSuanyangTechnology/feature/ui_upgrade_zy

fix(web): JinjaRender support third variable
This commit is contained in:
yingzhao
2026-04-08 14:46:39 +08:00
committed by GitHub
3 changed files with 109 additions and 21 deletions

View File

@@ -4,7 +4,7 @@
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-07 14:50:14 * @Last Modified time: 2026-04-07 14:50:14
*/ */
import { useEffect, useState, useRef, type FC } from 'react'; import { useEffect, useLayoutEffect, useState, useRef, type FC } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { import {
$getSelection, $isRangeSelection, $isTextNode, $getSelection, $isRangeSelection, $isTextNode,
@@ -20,8 +20,35 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, anchorBottom: 0 });
const [expandedParent, setExpandedParent] = useState<Suggestion | null>(null);
const [childPanelTop, setChildPanelTop] = useState(0);
const popupRef = useRef<HTMLDivElement>(null); const popupRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<Map<string, HTMLElement>>(new Map());
const CHILD_PANEL_HEIGHT = 280;
useLayoutEffect(() => {
if (!popupRef.current || !showSuggestions) return;
const { top, anchorBottom } = popupPosition;
const popupHeight = popupRef.current.offsetHeight;
const MARGIN = 10;
let finalTop: number;
if (top - popupHeight - MARGIN >= 0) {
finalTop = top - popupHeight - MARGIN;
} else {
finalTop = anchorBottom + MARGIN;
if (finalTop + popupHeight > window.innerHeight - MARGIN)
finalTop = window.innerHeight - popupHeight - MARGIN;
}
if (finalTop !== top) setPopupPosition(prev => ({ ...prev, top: finalTop }));
}, [showSuggestions, popupPosition.anchorBottom]);
const calcChildPanelTop = (elRect: DOMRect, popupRect: DOMRect) => {
const relativeTop = elRect.top - popupRect.top;
const overflow = popupRect.top + relativeTop + CHILD_PANEL_HEIGHT - (window.innerHeight - 10);
return overflow > 0 ? relativeTop - overflow : relativeTop;
};
const scrollSelectedIntoView = () => { const scrollSelectedIntoView = () => {
if (!popupRef.current) return; if (!popupRef.current) return;
@@ -51,19 +78,16 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
const textBeforeCursor = anchorNode.getTextContent().substring(0, anchorOffset); const textBeforeCursor = anchorNode.getTextContent().substring(0, anchorOffset);
const shouldShow = textBeforeCursor.endsWith('/'); const shouldShow = textBeforeCursor.endsWith('/');
setShowSuggestions(shouldShow); setShowSuggestions(shouldShow);
if (!shouldShow) { setSelectedIndex(0); return; } if (!shouldShow) { setSelectedIndex(0); setExpandedParent(null); setChildPanelTop(0); return; }
const domSelection = window.getSelection(); const domSelection = window.getSelection();
if (domSelection && domSelection.rangeCount > 0) { if (domSelection && domSelection.rangeCount > 0) {
const rect = domSelection.getRangeAt(0).getBoundingClientRect(); const rect = domSelection.getRangeAt(0).getBoundingClientRect();
const popupWidth = 280, popupHeight = 200; const popupWidth = 280;
const vw = window.innerWidth, vh = window.innerHeight; let left = rect.left;
let left = Math.min(Math.max(rect.left, 10), vw - popupWidth - 10); if (left + popupWidth > window.innerWidth) left = window.innerWidth - popupWidth - 10;
let top = rect.top - 10; if (left < 10) left = 10;
if (top - popupHeight < 10) { setPopupPosition({ top: rect.top, left, anchorBottom: rect.bottom });
top = Math.min(rect.bottom + 10, vh - popupHeight - 10);
}
setPopupPosition({ top, left });
} }
}); });
}); });
@@ -72,7 +96,7 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
useEffect(() => { useEffect(() => {
return editor.registerCommand( return editor.registerCommand(
CLOSE_AUTOCOMPLETE_COMMAND, CLOSE_AUTOCOMPLETE_COMMAND,
() => { setShowSuggestions(false); return true; }, () => { setShowSuggestions(false); setExpandedParent(null); setChildPanelTop(0); return true; },
COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_HIGH,
); );
}, [editor]); }, [editor]);
@@ -95,6 +119,8 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
} }
}); });
setShowSuggestions(false); setShowSuggestions(false);
setExpandedParent(null);
setChildPanelTop(0);
}; };
const groupedSuggestions = options.reduce((groups: Record<string, Suggestion[]>, s) => { const groupedSuggestions = options.reduce((groups: Record<string, Suggestion[]>, s) => {
@@ -104,7 +130,9 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
return groups; return groups;
}, {}); }, {});
const allOptions = Object.values(groupedSuggestions).flat(); const allOptions = Object.values(groupedSuggestions).flat().flatMap(o =>
o.key === expandedParent?.key && o.children?.length ? [o, ...o.children] : [o]
);
useEffect(() => { useEffect(() => {
if (!showSuggestions) return; if (!showSuggestions) return;
@@ -154,9 +182,10 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
ref={popupRef} ref={popupRef}
data-autocomplete-popup="true" data-autocomplete-popup="true"
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => 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)]" className="rb:fixed rb:z-1000 rb:bg-white rb:rounded-xl rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]"
style={{ top: popupPosition.top, left: popupPosition.left }} style={{ top: popupPosition.top, left: popupPosition.left }}
> >
<div className="rb:py-1 rb:min-w-70 rb:max-h-50 rb:overflow-y-auto">
<Flex vertical gap={12}> <Flex vertical gap={12}>
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => ( {Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => (
<div key={nodeId}> <div key={nodeId}>
@@ -166,32 +195,86 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
</Flex> </Flex>
{nodeOptions.map((option) => { {nodeOptions.map((option) => {
const globalIndex = allOptions.indexOf(option); const globalIndex = allOptions.indexOf(option);
const hasChildren = !!option.children?.length;
const isExpanded = expandedParent?.key === option.key;
return ( return (
<Flex <Flex
key={option.key} key={option.key}
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
data-selected={selectedIndex === globalIndex} data-selected={selectedIndex === globalIndex}
className="rb:pl-6! rb:pr-3! rb:py-2!" className="rb:pl-6! rb:pr-3! rb:py-2!"
align="center" align="center"
justify="space-between" justify="space-between"
style={{ style={{
cursor: option.disabled ? 'not-allowed' : 'pointer', cursor: option.disabled ? 'not-allowed' : 'pointer',
background: selectedIndex === globalIndex ? '#f0f8ff' : 'white', background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white',
opacity: option.disabled ? 0.5 : 1, opacity: option.disabled ? 0.5 : 1,
}} }}
onClick={() => !option.disabled && insertMention(option)} onClick={() => { if (option.disabled || hasChildren) return; insertMention(option); }}
onMouseEnter={() => setSelectedIndex(globalIndex)} onMouseEnter={() => {
setSelectedIndex(globalIndex);
if (hasChildren) {
const el = itemRefs.current.get(option.key);
if (el && popupRef.current) {
setChildPanelTop(calcChildPanelTop(el.getBoundingClientRect(), popupRef.current.getBoundingClientRect()));
}
setExpandedParent(option);
} else {
setExpandedParent(null);
}
}}
> >
<Space size={4}> <Space size={4}>
<span className="rb:text-[#155EEF]">{option.isContext ? '📄' : '{x}'}</span> <span className="rb:text-[#155EEF]">{option.isContext ? '📄' : '{x}'}</span>
<span>{option.label}</span> <span>{option.label}</span>
</Space> </Space>
{option.dataType && <span className="rb:text-[#5B6167]">{option.dataType}</span>} <Space size={4}>
{option.dataType && <span className="rb:text-[#5B6167]">{option.dataType}</span>}
{hasChildren && <span className="rb:text-[#5B6167] rb:ml-1"></span>}
</Space>
</Flex> </Flex>
); );
})} })}
</div> </div>
))} ))}
</Flex> </Flex>
</div>
{expandedParent?.children?.length && (
<div
className="rb:absolute rb:bg-white rb:rounded-xl rb:py-1 rb:min-w-60 rb:max-h-60 rb:overflow-y-auto rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]"
style={{ top: childPanelTop, right: 'calc(100% + 8px)', transform: 'translateY(-8px)' }}
onMouseEnter={() => setExpandedParent(expandedParent)}
>
<div className="rb:px-3 rb:py-2 rb:text-[12px] rb:font-medium rb:text-[#5B6167] rb:border-b rb:border-[#F0F0F0]">
<Flex justify="space-between" align="center">
<span>{expandedParent.nodeData.name}.{expandedParent.label}</span>
<span>{expandedParent.dataType}</span>
</Flex>
</div>
{expandedParent.children.map((child) => {
const childIndex = allOptions.indexOf(child);
return (
<Flex
key={child.key}
data-selected={selectedIndex === childIndex}
className="rb:px-3! rb:py-2!"
align="center"
justify="space-between"
style={{
cursor: child.disabled ? 'not-allowed' : 'pointer',
background: selectedIndex === childIndex ? '#f0f8ff' : 'white',
opacity: child.disabled ? 0.5 : 1,
}}
onClick={() => !child.disabled && insertMention(child)}
onMouseEnter={() => setSelectedIndex(childIndex)}
>
<span>{child.label}</span>
{child.dataType && <span className="rb:text-[#5B6167]">{child.dataType}</span>}
</Flex>
);
})}
</div>
)}
</div> </div>
); );
}; };

View File

@@ -140,7 +140,13 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
if (existingMapping) { if (existingMapping) {
updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`) updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`)
} else if (!existingNames.includes(varName)) { } else if (!existingNames.includes(varName)) {
const mappingName = varName.includes('.') ? varName.split('.').pop() || varName : varName const baseName = varName.includes('.') ? varName.split('.').pop() || varName : varName
const usedNames = getMappingNames(updatedMapping)
let mappingName = baseName
let counter = 1
while (usedNames.includes(mappingName)) {
mappingName = `${baseName}_${counter++}`
}
updatedMapping.push({ name: mappingName, value: `{{${varName}}}` }) updatedMapping.push({ name: mappingName, value: `{{${varName}}}` })
updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`) updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`)
} }

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 15:39:59 * @Date: 2026-02-03 15:39:59
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-27 11:30:44 * @Last Modified time: 2026-04-08 14:10:40
*/ */
import { type FC, useEffect, useState, useMemo } from "react"; import { type FC, useEffect, useState, useMemo } from "react";
import clsx from 'clsx' import clsx from 'clsx'
@@ -38,7 +38,6 @@ import RbCard from '@/components/RbCard/Card';
import ModelConfig from './ModelConfig' import ModelConfig from './ModelConfig'
import ModelSelect from '@/components/ModelSelect' import ModelSelect from '@/components/ModelSelect'
import ListOperator from './ListOperator' import ListOperator from './ListOperator'
import type { Variable } from "./VariableList/types";
/** /**
* Props for Properties component * Props for Properties component