mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-12 06:29:03 +08:00
feat: code transform node editor support insert var by add slash or left brace (#3946)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
This commit is contained in:
parent
e7b4d024ee
commit
3e992cb23c
@ -38,6 +38,7 @@ const RunMode = memo(() => {
|
|||||||
const {
|
const {
|
||||||
doSyncWorkflowDraft,
|
doSyncWorkflowDraft,
|
||||||
} = useNodesSyncDraft()
|
} = useNodesSyncDraft()
|
||||||
|
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||||
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||||
|
|
||||||
@ -55,10 +56,16 @@ const RunMode = memo(() => {
|
|||||||
const startVariables = startNode?.data.variables || []
|
const startVariables = startNode?.data.variables || []
|
||||||
const fileSettings = featuresStore!.getState().features.file
|
const fileSettings = featuresStore!.getState().features.file
|
||||||
const {
|
const {
|
||||||
|
showDebugAndPreviewPanel,
|
||||||
setShowDebugAndPreviewPanel,
|
setShowDebugAndPreviewPanel,
|
||||||
setShowInputsPanel,
|
setShowInputsPanel,
|
||||||
} = workflowStore.getState()
|
} = workflowStore.getState()
|
||||||
|
|
||||||
|
if (showDebugAndPreviewPanel) {
|
||||||
|
handleCancelDebugAndPreviewPanel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!startVariables.length && !fileSettings?.image?.enabled) {
|
if (!startVariables.length && !fileSettings?.image?.enabled) {
|
||||||
await doSyncWorkflowDraft()
|
await doSyncWorkflowDraft()
|
||||||
handleRun({ inputs: {}, files: [] })
|
handleRun({ inputs: {}, files: [] })
|
||||||
@ -75,6 +82,7 @@ const RunMode = memo(() => {
|
|||||||
doSyncWorkflowDraft,
|
doSyncWorkflowDraft,
|
||||||
store,
|
store,
|
||||||
featuresStore,
|
featuresStore,
|
||||||
|
handleCancelDebugAndPreviewPanel,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -21,6 +21,7 @@ export const useWorkflowInteractions = () => {
|
|||||||
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
||||||
workflowStore.setState({
|
workflowStore.setState({
|
||||||
showDebugAndPreviewPanel: false,
|
showDebugAndPreviewPanel: false,
|
||||||
|
workflowRunningData: undefined,
|
||||||
})
|
})
|
||||||
handleNodeCancelRunningStatus()
|
handleNodeCancelRunningStatus()
|
||||||
handleEdgeCancelRunningStatus()
|
handleEdgeCancelRunningStatus()
|
||||||
|
@ -0,0 +1,173 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useBoolean } from 'ahooks'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import type { Props as EditorProps } from '.'
|
||||||
|
import Editor from '.'
|
||||||
|
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||||
|
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||||
|
import type { Variable } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
const TO_WINDOW_OFFSET = 8
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nodeId: string
|
||||||
|
varList: Variable[]
|
||||||
|
onAddVar: (payload: Variable) => void
|
||||||
|
} & EditorProps
|
||||||
|
|
||||||
|
const CodeEditor: FC<Props> = ({
|
||||||
|
nodeId,
|
||||||
|
varList,
|
||||||
|
onAddVar,
|
||||||
|
...editorProps
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { availableVars } = useAvailableVarList(nodeId, {
|
||||||
|
onlyLeafNodeVar: false,
|
||||||
|
filterVar: () => true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLeftBraceRef = useRef(false)
|
||||||
|
|
||||||
|
const editorRef = useRef(null)
|
||||||
|
const monacoRef = useRef(null)
|
||||||
|
|
||||||
|
const popupRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [isShowVarPicker, {
|
||||||
|
setTrue: showVarPicker,
|
||||||
|
setFalse: hideVarPicker,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
|
||||||
|
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
// Listen for cursor position changes
|
||||||
|
const handleCursorPositionChange = (event: any) => {
|
||||||
|
const editor: any = editorRef.current
|
||||||
|
const { position } = event
|
||||||
|
const text = editor.getModel().getLineContent(position.lineNumber)
|
||||||
|
const charBefore = text[position.column - 2]
|
||||||
|
if (['/', '{'].includes(charBefore)) {
|
||||||
|
isLeftBraceRef.current = charBefore === '{'
|
||||||
|
const editorRect = editor.getDomNode().getBoundingClientRect()
|
||||||
|
const cursorCoords = editor.getScrolledVisiblePosition(position)
|
||||||
|
|
||||||
|
const popupX = editorRect.left + cursorCoords.left
|
||||||
|
const popupY = editorRect.top + cursorCoords.top + 20 // Adjust the vertical position as needed
|
||||||
|
|
||||||
|
setPopupPosition({ x: popupX, y: popupY })
|
||||||
|
showVarPicker()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
hideVarPicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isShowVarPicker && popupRef.current) {
|
||||||
|
const windowWidth = window.innerWidth
|
||||||
|
const { width, height } = popupRef.current!.getBoundingClientRect()
|
||||||
|
const newPopupPosition = { ...popupPosition }
|
||||||
|
if (popupPosition.x + width > windowWidth - TO_WINDOW_OFFSET)
|
||||||
|
newPopupPosition.x = windowWidth - width - TO_WINDOW_OFFSET
|
||||||
|
|
||||||
|
if (popupPosition.y + height > window.innerHeight - TO_WINDOW_OFFSET)
|
||||||
|
newPopupPosition.y = window.innerHeight - height - TO_WINDOW_OFFSET
|
||||||
|
|
||||||
|
setPopupPosition(newPopupPosition)
|
||||||
|
}
|
||||||
|
}, [isShowVarPicker, popupPosition])
|
||||||
|
|
||||||
|
const onEditorMounted = (editor: any, monaco: any) => {
|
||||||
|
editorRef.current = editor
|
||||||
|
monacoRef.current = monaco
|
||||||
|
editor.onDidChangeCursorPosition(handleCursorPositionChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUniqVarName = (varName: string) => {
|
||||||
|
if (varList.find(v => v.variable === varName)) {
|
||||||
|
const match = varName.match(/_([0-9]+)$/)
|
||||||
|
|
||||||
|
const index = (() => {
|
||||||
|
if (match)
|
||||||
|
return parseInt(match[1]!) + 1
|
||||||
|
|
||||||
|
return 1
|
||||||
|
})()
|
||||||
|
return getUniqVarName(`${varName.replace(/_([0-9]+)$/, '')}_${index}`)
|
||||||
|
}
|
||||||
|
return varName
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVarName = (varValue: string[]) => {
|
||||||
|
const existVar = varList.find(v => Array.isArray(v.value_selector) && v.value_selector.join('@@@') === varValue.join('@@@'))
|
||||||
|
if (existVar) {
|
||||||
|
return {
|
||||||
|
name: existVar.variable,
|
||||||
|
isExist: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const varName = varValue.slice(-1)[0]
|
||||||
|
return {
|
||||||
|
name: getUniqVarName(varName),
|
||||||
|
isExist: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectVar = (varValue: string[]) => {
|
||||||
|
const { name, isExist } = getVarName(varValue)
|
||||||
|
if (!isExist) {
|
||||||
|
const newVar: Variable = {
|
||||||
|
variable: name,
|
||||||
|
value_selector: varValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddVar(newVar)
|
||||||
|
}
|
||||||
|
const editor: any = editorRef.current
|
||||||
|
const monaco: any = monacoRef.current
|
||||||
|
const position = editor?.getPosition()
|
||||||
|
|
||||||
|
// Insert the content at the cursor position
|
||||||
|
editor?.executeEdits('', [
|
||||||
|
{
|
||||||
|
// position.column - 1 to remove the text before the cursor
|
||||||
|
range: new monaco.Range(position.lineNumber, position.column - 1, position.lineNumber, position.column),
|
||||||
|
text: `{{ ${name} }${!isLeftBraceRef.current ? '}' : ''}`, // left brace would auto add one right brace
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
hideVarPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Editor
|
||||||
|
{...editorProps}
|
||||||
|
onMount={onEditorMounted}
|
||||||
|
placeholder={t('workflow.common.jinjaEditorPlaceholder')!}
|
||||||
|
/>
|
||||||
|
{isShowVarPicker && (
|
||||||
|
<div
|
||||||
|
ref={popupRef}
|
||||||
|
className='w-[228px] p-1 bg-white rounded-lg border border-gray-200 shadow-lg space-y-1'
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: popupPosition.y,
|
||||||
|
left: popupPosition.x,
|
||||||
|
zIndex: 100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VarReferenceVars
|
||||||
|
hideSearch
|
||||||
|
vars={availableVars}
|
||||||
|
onChange={handleSelectVar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(CodeEditor)
|
@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import Editor, { loader } from '@monaco-editor/react'
|
import Editor, { loader } from '@monaco-editor/react'
|
||||||
|
|
||||||
import React, { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import Base from '../base'
|
import Base from '../base'
|
||||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||||
@ -9,8 +10,9 @@ import './style.css'
|
|||||||
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
|
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
|
||||||
loader.config({ paths: { vs: '/vs' } })
|
loader.config({ paths: { vs: '/vs' } })
|
||||||
|
|
||||||
type Props = {
|
export type Props = {
|
||||||
value?: string | object
|
value?: string | object
|
||||||
|
placeholder?: string
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
title: JSX.Element
|
title: JSX.Element
|
||||||
language: CodeLanguage
|
language: CodeLanguage
|
||||||
@ -19,6 +21,7 @@ type Props = {
|
|||||||
isJSONStringifyBeauty?: boolean
|
isJSONStringifyBeauty?: boolean
|
||||||
height?: number
|
height?: number
|
||||||
isInNode?: boolean
|
isInNode?: boolean
|
||||||
|
onMount?: (editor: any, monaco: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageMap = {
|
const languageMap = {
|
||||||
@ -29,6 +32,7 @@ const languageMap = {
|
|||||||
|
|
||||||
const CodeEditor: FC<Props> = ({
|
const CodeEditor: FC<Props> = ({
|
||||||
value = '',
|
value = '',
|
||||||
|
placeholder = '',
|
||||||
onChange = () => { },
|
onChange = () => { },
|
||||||
title,
|
title,
|
||||||
headerRight,
|
headerRight,
|
||||||
@ -37,6 +41,7 @@ const CodeEditor: FC<Props> = ({
|
|||||||
isJSONStringifyBeauty,
|
isJSONStringifyBeauty,
|
||||||
height,
|
height,
|
||||||
isInNode,
|
isInNode,
|
||||||
|
onMount,
|
||||||
}) => {
|
}) => {
|
||||||
const [isFocus, setIsFocus] = React.useState(false)
|
const [isFocus, setIsFocus] = React.useState(false)
|
||||||
|
|
||||||
@ -47,6 +52,7 @@ const CodeEditor: FC<Props> = ({
|
|||||||
const editorRef = useRef(null)
|
const editorRef = useRef(null)
|
||||||
const handleEditorDidMount = (editor: any, monaco: any) => {
|
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||||||
editorRef.current = editor
|
editorRef.current = editor
|
||||||
|
|
||||||
editor.onDidFocusEditorText(() => {
|
editor.onDidFocusEditorText(() => {
|
||||||
setIsFocus(true)
|
setIsFocus(true)
|
||||||
})
|
})
|
||||||
@ -71,6 +77,8 @@ const CodeEditor: FC<Props> = ({
|
|||||||
'editor.background': '#ffffff',
|
'editor.background': '#ffffff',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount?.(editor, monaco)
|
||||||
}
|
}
|
||||||
|
|
||||||
const outPutValue = (() => {
|
const outPutValue = (() => {
|
||||||
@ -87,6 +95,7 @@ const CodeEditor: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Base
|
<Base
|
||||||
|
className='relative'
|
||||||
title={title}
|
title={title}
|
||||||
value={outPutValue}
|
value={outPutValue}
|
||||||
headerRight={headerRight}
|
headerRight={headerRight}
|
||||||
@ -117,6 +126,7 @@ const CodeEditor: FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
onMount={handleEditorDidMount}
|
onMount={handleEditorDidMount}
|
||||||
/>
|
/>
|
||||||
|
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
|
||||||
</>
|
</>
|
||||||
</Base>
|
</Base>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ import VarList from '@/app/components/workflow/nodes/_base/components/variable/v
|
|||||||
import AddButton from '@/app/components/base/button/add-button'
|
import AddButton from '@/app/components/base/button/add-button'
|
||||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars'
|
||||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||||
@ -28,6 +28,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
|
|||||||
inputs,
|
inputs,
|
||||||
handleVarListChange,
|
handleVarListChange,
|
||||||
handleAddVariable,
|
handleAddVariable,
|
||||||
|
handleAddEmptyVariable,
|
||||||
handleCodeChange,
|
handleCodeChange,
|
||||||
filterVar,
|
filterVar,
|
||||||
// single run
|
// single run
|
||||||
@ -49,7 +50,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
|
|||||||
<Field
|
<Field
|
||||||
title={t(`${i18nPrefix}.inputVars`)}
|
title={t(`${i18nPrefix}.inputVars`)}
|
||||||
operations={
|
operations={
|
||||||
!readOnly ? <AddButton onClick={handleAddVariable} /> : undefined
|
!readOnly ? <AddButton onClick={handleAddEmptyVariable} /> : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<VarList
|
<VarList
|
||||||
@ -62,6 +63,9 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
|
|||||||
</Field>
|
</Field>
|
||||||
<Split />
|
<Split />
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
nodeId={id}
|
||||||
|
varList={inputs.variables}
|
||||||
|
onAddVar={handleAddVariable}
|
||||||
isInNode
|
isInNode
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
language={CodeLanguage.python3}
|
language={CodeLanguage.python3}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import useVarList from '../_base/hooks/use-var-list'
|
import useVarList from '../_base/hooks/use-var-list'
|
||||||
import type { Var } from '../../types'
|
import type { Var, Variable } from '../../types'
|
||||||
import { VarType } from '../../types'
|
import { VarType } from '../../types'
|
||||||
import { useStore } from '../../store'
|
import { useStore } from '../../store'
|
||||||
import type { TemplateTransformNodeType } from './types'
|
import type { TemplateTransformNodeType } from './types'
|
||||||
@ -15,12 +15,25 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
|
|||||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||||
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
|
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
|
||||||
|
|
||||||
const { inputs, setInputs } = useNodeCrud<TemplateTransformNodeType>(id, payload)
|
const { inputs, setInputs: doSetInputs } = useNodeCrud<TemplateTransformNodeType>(id, payload)
|
||||||
const { handleVarListChange, handleAddVariable } = useVarList<TemplateTransformNodeType>({
|
const inputsRef = useRef(inputs)
|
||||||
|
const setInputs = useCallback((newPayload: TemplateTransformNodeType) => {
|
||||||
|
doSetInputs(newPayload)
|
||||||
|
inputsRef.current = newPayload
|
||||||
|
}, [doSetInputs])
|
||||||
|
|
||||||
|
const { handleVarListChange, handleAddVariable: handleAddEmptyVariable } = useVarList<TemplateTransformNodeType>({
|
||||||
inputs,
|
inputs,
|
||||||
setInputs,
|
setInputs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleAddVariable = useCallback((payload: Variable) => {
|
||||||
|
const newInputs = produce(inputsRef.current, (draft: any) => {
|
||||||
|
draft.variables.push(payload)
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}, [setInputs])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputs.template)
|
if (inputs.template)
|
||||||
return
|
return
|
||||||
@ -36,11 +49,11 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
|
|||||||
}, [defaultConfig])
|
}, [defaultConfig])
|
||||||
|
|
||||||
const handleCodeChange = useCallback((template: string) => {
|
const handleCodeChange = useCallback((template: string) => {
|
||||||
const newInputs = produce(inputs, (draft: any) => {
|
const newInputs = produce(inputsRef.current, (draft: any) => {
|
||||||
draft.template = template
|
draft.template = template
|
||||||
})
|
})
|
||||||
setInputs(newInputs)
|
setInputs(newInputs)
|
||||||
}, [inputs, setInputs])
|
}, [setInputs])
|
||||||
|
|
||||||
// single run
|
// single run
|
||||||
const {
|
const {
|
||||||
@ -82,6 +95,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
|
|||||||
inputs,
|
inputs,
|
||||||
handleVarListChange,
|
handleVarListChange,
|
||||||
handleAddVariable,
|
handleAddVariable,
|
||||||
|
handleAddEmptyVariable,
|
||||||
handleCodeChange,
|
handleCodeChange,
|
||||||
filterVar,
|
filterVar,
|
||||||
// single run
|
// single run
|
||||||
|
@ -49,6 +49,7 @@ const translation = {
|
|||||||
processData: 'Process Data',
|
processData: 'Process Data',
|
||||||
input: 'Input',
|
input: 'Input',
|
||||||
output: 'Output',
|
output: 'Output',
|
||||||
|
jinjaEditorPlaceholder: 'Type \'/\' or \'{\' to insert variable',
|
||||||
viewOnly: 'View Only',
|
viewOnly: 'View Only',
|
||||||
showRunHistory: 'Show Run History',
|
showRunHistory: 'Show Run History',
|
||||||
},
|
},
|
||||||
|
@ -49,6 +49,7 @@ const translation = {
|
|||||||
processData: '数据处理',
|
processData: '数据处理',
|
||||||
input: '输入',
|
input: '输入',
|
||||||
output: '输出',
|
output: '输出',
|
||||||
|
jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量',
|
||||||
viewOnly: '只读',
|
viewOnly: '只读',
|
||||||
showRunHistory: '显示运行历史',
|
showRunHistory: '显示运行历史',
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user