refactor: extract jinja render's form
This commit is contained in:
@@ -16,6 +16,7 @@ import InitialValuePlugin from './plugin/InitialValuePlugin';
|
|||||||
import CommandPlugin from './plugin/CommandPlugin';
|
import CommandPlugin from './plugin/CommandPlugin';
|
||||||
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||||
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
||||||
|
import BlurPlugin from './plugin/BlurPlugin';
|
||||||
import { VariableNode } from './nodes/VariableNode'
|
import { VariableNode } from './nodes/VariableNode'
|
||||||
|
|
||||||
interface LexicalEditorProps {
|
interface LexicalEditorProps {
|
||||||
@@ -113,8 +114,10 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
.editor-content-with-numbers {
|
.editor-content-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
}
|
||||||
|
.editor-content-with-numbers {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
.editor-content-with-numbers p {
|
.editor-content-with-numbers p {
|
||||||
@@ -174,18 +177,20 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<div className="line-numbers">
|
<div className="line-numbers">
|
||||||
<div>1</div>
|
<div>1</div>
|
||||||
</div>
|
</div>
|
||||||
<ContentEditable
|
<div className="editor-content-wrapper">
|
||||||
className="editor-content-with-numbers"
|
<ContentEditable
|
||||||
style={{
|
className="editor-content-with-numbers"
|
||||||
minHeight: minheight,
|
style={{
|
||||||
padding: '4px 0',
|
minHeight: minheight,
|
||||||
outline: 'none',
|
padding: '4px 0',
|
||||||
resize: 'none',
|
outline: 'none',
|
||||||
fontSize: fontSize,
|
resize: 'none',
|
||||||
lineHeight: lineHeight,
|
fontSize: fontSize,
|
||||||
border: 'none',
|
lineHeight: lineHeight,
|
||||||
}}
|
border: 'none',
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
@@ -207,8 +212,8 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
style={{
|
style={{
|
||||||
minHeight: placeHolderMinheight,
|
minHeight: placeHolderMinheight,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: variant === 'borderless' ? '0' : '6px',
|
top: enableJinja2 ? '4px' : variant === 'borderless' ? '0' : '6px',
|
||||||
left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'),
|
left: enableJinja2 ? '16px' : (variant === 'borderless' ? '0' : '11px'),
|
||||||
color: '#A8A9AA',
|
color: '#A8A9AA',
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
lineHeight: placeHolderMinheight,
|
lineHeight: placeHolderMinheight,
|
||||||
@@ -227,6 +232,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||||
|
{enableJinja2 && <BlurPlugin />}
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={clsx('rb:border rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:py-0.5 rb:px-1.5 rb:mx-0.5 rb:cursor-pointer', {
|
className={clsx('rb:border rb:rounded-md rb:bg-white rb:text-[10px] rb:inline-flex rb:items-center rb:py-0 rb:px-1.5 rb:mx-0.5 rb:cursor-pointer', {
|
||||||
'rb:border-[#155EEF]': isSelected,
|
'rb:border-[#155EEF]': isSelected,
|
||||||
'rb:border-[#DFE4ED]': !isSelected
|
'rb:border-[#DFE4ED]': !isSelected
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, type FC } from 'react';
|
import { useEffect, useState, type FC } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { $getSelection, $isRangeSelection } from 'lexical';
|
import { $getSelection, $isRangeSelection, $isTextNode } from 'lexical';
|
||||||
|
|
||||||
import { INSERT_VARIABLE_COMMAND } from '../commands';
|
import { INSERT_VARIABLE_COMMAND } from '../commands';
|
||||||
import type { NodeProperties } from '../../../types'
|
import type { NodeProperties } from '../../../types'
|
||||||
@@ -96,7 +96,9 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
const textAfter = nodeText.substring(anchorOffset);
|
const textAfter = nodeText.substring(anchorOffset);
|
||||||
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
|
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
|
||||||
|
|
||||||
anchorNode.setTextContent(newText);
|
if ($isTextNode(anchorNode)) {
|
||||||
|
anchorNode.setTextContent(newText);
|
||||||
|
}
|
||||||
|
|
||||||
// 设置光标位置到插入文本之后
|
// 设置光标位置到插入文本之后
|
||||||
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
|
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
|
||||||
@@ -129,6 +131,8 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-autocomplete-popup="true"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: popupPosition.top,
|
top: popupPosition.top,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { $setSelection } from 'lexical';
|
||||||
|
|
||||||
|
export default function BlurPlugin() {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerRootListener((rootElement) => {
|
||||||
|
if (rootElement) {
|
||||||
|
const handleBlur = (e: FocusEvent) => {
|
||||||
|
// 检查是否点击了自动完成弹窗
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
console.log('target', target)
|
||||||
|
if (target?.closest('[data-autocomplete-popup="true"]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.update(() => {
|
||||||
|
$setSelection(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
rootElement.addEventListener('blur', handleBlur);
|
||||||
|
return () => {
|
||||||
|
rootElement.removeEventListener('blur', handleBlur);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { type FC, useEffect, useRef } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Form } from 'antd'
|
||||||
|
import { Node } from '@antv/x6'
|
||||||
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
|
import MappingList from '../MappingList'
|
||||||
|
import MessageEditor from '../MessageEditor'
|
||||||
|
|
||||||
|
interface MappingItem {
|
||||||
|
name?: string
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JinjaRenderProps {
|
||||||
|
options: Suggestion[]
|
||||||
|
templateOptions: Suggestion[]
|
||||||
|
selectedNode: Node
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractTemplateVars = (template: string): string[] => {
|
||||||
|
return (template.match(/{{\s*([\w.]+)\s*}}/g) || [])
|
||||||
|
.map(m => m.replace(/{{\s*|\s*}}/g, ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMappingNames = (mapping: MappingItem[]): string[] => {
|
||||||
|
return mapping.filter(item => item?.name).map(item => item.name!)
|
||||||
|
}
|
||||||
|
|
||||||
|
const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOptions }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const form = Form.useFormInstance()
|
||||||
|
const values = Form.useWatch([], form) || {}
|
||||||
|
|
||||||
|
console.log('JinjaRender values', values)
|
||||||
|
|
||||||
|
const prevMappingNamesRef = useRef<string[]>([])
|
||||||
|
const prevTemplateVarsRef = useRef<string[]>([])
|
||||||
|
const syncTimeoutRef = useRef<number | null>(null)
|
||||||
|
const isSyncingRef = useRef(false)
|
||||||
|
const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null)
|
||||||
|
|
||||||
|
// Reset refs when node changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedNode?.getData()?.id) {
|
||||||
|
prevMappingNamesRef.current = []
|
||||||
|
prevTemplateVarsRef.current = []
|
||||||
|
lastSyncSourceRef.current = null
|
||||||
|
}
|
||||||
|
}, [selectedNode?.getData()?.id])
|
||||||
|
|
||||||
|
// Sync template when mapping names change
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isSyncingRef.current ||
|
||||||
|
lastSyncSourceRef.current === 'mapping' ||
|
||||||
|
selectedNode?.data?.type !== 'jinja-render' ||
|
||||||
|
!values?.mapping ||
|
||||||
|
!values?.template
|
||||||
|
) return
|
||||||
|
|
||||||
|
const currentMappingNames = Array.isArray(values.mapping) ? getMappingNames(values.mapping) : []
|
||||||
|
const prevNames = prevMappingNamesRef.current
|
||||||
|
|
||||||
|
if (prevNames.length === 0) {
|
||||||
|
prevMappingNamesRef.current = currentMappingNames
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return
|
||||||
|
|
||||||
|
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current)
|
||||||
|
const activeElement = document.activeElement as HTMLElement
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}, [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' ||
|
||||||
|
selectedNode?.data?.type !== 'jinja-render' ||
|
||||||
|
!values?.template ||
|
||||||
|
!values?.mapping
|
||||||
|
) return
|
||||||
|
|
||||||
|
const templateVars = extractTemplateVars(String(values.template))
|
||||||
|
if (JSON.stringify(prevTemplateVarsRef.current) === JSON.stringify(templateVars)) return
|
||||||
|
|
||||||
|
const isTemplateEditor = document.activeElement?.closest('[data-editor-type="template"]')
|
||||||
|
if (!isTemplateEditor) {
|
||||||
|
prevTemplateVarsRef.current = templateVars
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMapping: MappingItem[] = Array.isArray(values.mapping)
|
||||||
|
? [...values.mapping.filter((item: MappingItem) => item)]
|
||||||
|
: []
|
||||||
|
const existingNames = getMappingNames(updatedMapping)
|
||||||
|
let updatedTemplate = String(values.template)
|
||||||
|
|
||||||
|
// Update existing mapping names based on position
|
||||||
|
if (prevTemplateVarsRef.current.length > 0) {
|
||||||
|
prevTemplateVarsRef.current.forEach((oldVar, index) => {
|
||||||
|
const newVar = templateVars[index]
|
||||||
|
if (newVar && oldVar !== newVar && updatedMapping[index]) {
|
||||||
|
updatedMapping[index] = { ...updatedMapping[index], name: newVar }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new mappings and normalize template
|
||||||
|
templateVars.forEach(varName => {
|
||||||
|
const existingMapping = updatedMapping.find(item => item.value === `{{${varName}}}`)
|
||||||
|
const regex = new RegExp(`{{\\s*${varName.replace(/\./g, '\\.')}\\s*}}`, 'g')
|
||||||
|
|
||||||
|
if (existingMapping) {
|
||||||
|
updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`)
|
||||||
|
} else if (!existingNames.includes(varName)) {
|
||||||
|
const mappingName = varName.includes('.') ? varName.split('.').pop() || varName : varName
|
||||||
|
updatedMapping.push({ name: mappingName, value: `{{${varName}}}` })
|
||||||
|
updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove unused mappings and duplicates
|
||||||
|
const seenNames = new Set<string>()
|
||||||
|
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
|
||||||
|
seenNames.add(item.name)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
isSyncingRef.current = true
|
||||||
|
lastSyncSourceRef.current = 'template'
|
||||||
|
prevMappingNamesRef.current = getMappingNames(finalMapping)
|
||||||
|
prevTemplateVarsRef.current = templateVars
|
||||||
|
|
||||||
|
if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) {
|
||||||
|
form.setFieldValue('mapping', finalMapping)
|
||||||
|
}
|
||||||
|
if (updatedTemplate !== String(values.template)) {
|
||||||
|
form.setFieldValue('template', updatedTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isSyncingRef.current = false
|
||||||
|
lastSyncSourceRef.current = null
|
||||||
|
}, 50)
|
||||||
|
}, [values?.template, selectedNode?.data?.type, form])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name="mapping" noStyle>
|
||||||
|
<MappingList name="mapping" options={options} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="template">
|
||||||
|
<MessageEditor
|
||||||
|
title={t('workflow.config.jinja-render.template')}
|
||||||
|
isArray={false}
|
||||||
|
parentName="template"
|
||||||
|
enableJinja2={true}
|
||||||
|
options={templateOptions}
|
||||||
|
titleVariant="borderless"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JinjaRender
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, useEffect, useState, useRef, useMemo } from "react";
|
import { type FC, useEffect, useState, useMemo } from "react";
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Graph, Node } from '@antv/x6';
|
import { Graph, Node } from '@antv/x6';
|
||||||
@@ -17,7 +17,6 @@ import ParamsList from './ParamsList';
|
|||||||
import GroupVariableList from './GroupVariableList'
|
import GroupVariableList from './GroupVariableList'
|
||||||
import CaseList from './CaseList'
|
import CaseList from './CaseList'
|
||||||
import HttpRequest from './HttpRequest';
|
import HttpRequest from './HttpRequest';
|
||||||
import MappingList from './MappingList'
|
|
||||||
import CategoryList from './CategoryList'
|
import CategoryList from './CategoryList'
|
||||||
import ConditionList from './ConditionList'
|
import ConditionList from './ConditionList'
|
||||||
import CycleVarsList from './CycleVarsList'
|
import CycleVarsList from './CycleVarsList'
|
||||||
@@ -29,6 +28,7 @@ import { useVariableList, getCurrentNodeVariables } from './hooks/useVariableLis
|
|||||||
import styles from './properties.module.css'
|
import styles from './properties.module.css'
|
||||||
import Editor from "../Editor";
|
import Editor from "../Editor";
|
||||||
import RbSlider from './RbSlider'
|
import RbSlider from './RbSlider'
|
||||||
|
import JinjaRender from './JinjaRender'
|
||||||
|
|
||||||
interface PropertiesProps {
|
interface PropertiesProps {
|
||||||
selectedNode?: Node | null;
|
selectedNode?: Node | null;
|
||||||
@@ -50,136 +50,16 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
const [form] = Form.useForm<NodeConfig>();
|
const [form] = Form.useForm<NodeConfig>();
|
||||||
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
|
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
|
||||||
const values = Form.useWatch([], form);
|
const values = Form.useWatch([], form);
|
||||||
const prevMappingNamesRef = useRef<string[]>([])
|
|
||||||
const prevTemplateVarsRef = useRef<string[]>([])
|
|
||||||
const syncTimeoutRef = useRef<number | null>(null)
|
|
||||||
const isSyncingRef = useRef(false)
|
|
||||||
const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null)
|
|
||||||
const variableList = useVariableList(selectedNode, graphRef, chatVariables)
|
const variableList = useVariableList(selectedNode, graphRef, chatVariables)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedNode?.getData()?.id) {
|
if (selectedNode?.getData()?.id) {
|
||||||
form.resetFields()
|
|
||||||
prevMappingNamesRef.current = []
|
|
||||||
prevTemplateVarsRef.current = []
|
|
||||||
lastSyncSourceRef.current = null
|
|
||||||
setOutputCollapsed(true)
|
setOutputCollapsed(true)
|
||||||
|
} else {
|
||||||
|
form.resetFields()
|
||||||
}
|
}
|
||||||
}, [selectedNode?.getData()?.id])
|
}, [selectedNode?.getData()?.id])
|
||||||
|
|
||||||
// Sync template when mapping names change
|
|
||||||
useEffect(() => {
|
|
||||||
if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return
|
|
||||||
|
|
||||||
const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.filter(item => item && item.name).map((item: any) => item.name) : []
|
|
||||||
const prevNames = prevMappingNamesRef.current
|
|
||||||
|
|
||||||
if (prevNames.length === 0) {
|
|
||||||
prevMappingNamesRef.current = currentMappingNames
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return
|
|
||||||
|
|
||||||
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current)
|
|
||||||
const activeElement = document.activeElement as HTMLElement
|
|
||||||
|
|
||||||
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'
|
|
||||||
const newTemplateVars = (updatedTemplate.match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, ''))
|
|
||||||
prevTemplateVarsRef.current = newTemplateVars
|
|
||||||
prevMappingNamesRef.current = currentMappingNames
|
|
||||||
form.setFieldValue('template', updatedTemplate)
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
activeElement?.focus?.()
|
|
||||||
setTimeout(() => {
|
|
||||||
isSyncingRef.current = false
|
|
||||||
lastSyncSourceRef.current = null
|
|
||||||
}, 50)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
prevMappingNamesRef.current = currentMappingNames
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}, [values?.mapping, selectedNode?.data?.type, form])
|
|
||||||
|
|
||||||
// Sync mapping when template variables change
|
|
||||||
useEffect(() => {
|
|
||||||
if (isSyncingRef.current || lastSyncSourceRef.current === 'template' || selectedNode?.data?.type !== 'jinja-render' || !values?.template || !values?.mapping) return
|
|
||||||
|
|
||||||
const templateVars = (String(values.template).match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, ''))
|
|
||||||
if (JSON.stringify(prevTemplateVarsRef.current) === JSON.stringify(templateVars)) return
|
|
||||||
|
|
||||||
const isTemplateEditor = document.activeElement?.closest('[data-editor-type="template"]')
|
|
||||||
if (!isTemplateEditor) {
|
|
||||||
prevTemplateVarsRef.current = templateVars
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping.filter(item => item)] : []
|
|
||||||
const existingNames = updatedMapping.filter(item => item && item.name).map(item => item.name)
|
|
||||||
let updatedTemplate = String(values.template)
|
|
||||||
|
|
||||||
if (prevTemplateVarsRef.current.length > 0) {
|
|
||||||
prevTemplateVarsRef.current.forEach((oldVar, index) => {
|
|
||||||
const newVar = templateVars[index]
|
|
||||||
if (newVar && oldVar !== newVar && updatedMapping[index]) {
|
|
||||||
updatedMapping[index] = { ...updatedMapping[index], name: newVar }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
templateVars.forEach(varName => {
|
|
||||||
const existingMapping = updatedMapping.find(item => item.value === `{{${varName}}}`)
|
|
||||||
const regex = new RegExp(`{{\\s*${varName.replace(/\./g, '\\.')}\\s*}}`, 'g')
|
|
||||||
|
|
||||||
if (existingMapping) {
|
|
||||||
updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`)
|
|
||||||
} else if (!existingNames.includes(varName)) {
|
|
||||||
const mappingName = varName.includes('.') ? varName.split('.').pop() || varName : varName
|
|
||||||
updatedMapping.push({ name: mappingName, value: `{{${varName}}}` })
|
|
||||||
updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const seenNames = new Set<string>()
|
|
||||||
const finalMapping = updatedMapping.filter(item => {
|
|
||||||
const isUsed = templateVars.some(v => item.name === v || item.value === `{{${v}}}`)
|
|
||||||
if (!isUsed || seenNames.has(item.name)) return false
|
|
||||||
seenNames.add(item.name)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
isSyncingRef.current = true
|
|
||||||
lastSyncSourceRef.current = 'template'
|
|
||||||
prevMappingNamesRef.current = finalMapping.filter(item => item && item.name).map((item: any) => item.name)
|
|
||||||
prevTemplateVarsRef.current = templateVars
|
|
||||||
|
|
||||||
if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) {
|
|
||||||
form.setFieldValue('mapping', finalMapping)
|
|
||||||
}
|
|
||||||
if (updatedTemplate !== String(values.template)) {
|
|
||||||
form.setFieldValue('template', updatedTemplate)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
isSyncingRef.current = false
|
|
||||||
lastSyncSourceRef.current = null
|
|
||||||
}, 50)
|
|
||||||
}, [values?.template, selectedNode?.data?.type, form])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedNode && form) {
|
if (selectedNode && form) {
|
||||||
const { type = 'default', name = '', config } = selectedNode.getData() || {}
|
const { type = 'default', name = '', config } = selectedNode.getData() || {}
|
||||||
@@ -197,6 +77,8 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
...initialValue,
|
...initialValue,
|
||||||
})
|
})
|
||||||
setConfigs(config || {})
|
setConfigs(config || {})
|
||||||
|
} else {
|
||||||
|
form.resetFields()
|
||||||
}
|
}
|
||||||
}, [selectedNode, form])
|
}, [selectedNode, form])
|
||||||
|
|
||||||
@@ -529,6 +411,12 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
/>
|
/>
|
||||||
: selectedNode?.data?.type === 'tool'
|
: selectedNode?.data?.type === 'tool'
|
||||||
? <ToolConfig options={variableList} />
|
? <ToolConfig options={variableList} />
|
||||||
|
: selectedNode?.data.type === 'jinja-render'
|
||||||
|
? <JinjaRender
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||||
|
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
|
||||||
|
/>
|
||||||
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||||
const config = configs[key] || {}
|
const config = configs[key] || {}
|
||||||
|
|
||||||
@@ -646,15 +534,6 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.type === 'mappingList') {
|
|
||||||
return (
|
|
||||||
<Form.Item key={key} name={key} noStyle>
|
|
||||||
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (config.type === 'cycleVarsList') {
|
if (config.type === 'cycleVarsList') {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={key} name={key}>
|
<Form.Item key={key} name={key}>
|
||||||
|
|||||||
Reference in New Issue
Block a user