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:
Joel 2024-04-28 17:51:58 +08:00 committed by GitHub
parent e7b4d024ee
commit 3e992cb23c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 221 additions and 9 deletions

View File

@ -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 (

View File

@ -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()

View File

@ -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)

View File

@ -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>

View File

@ -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}

View File

@ -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

View File

@ -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',
}, },

View File

@ -49,6 +49,7 @@ const translation = {
processData: '数据处理', processData: '数据处理',
input: '输入', input: '输入',
output: '输出', output: '输出',
jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量',
viewOnly: '只读', viewOnly: '只读',
showRunHistory: '显示运行历史', showRunHistory: '显示运行历史',
}, },