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
8 changed files with 221 additions and 9 deletions

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'
import type { FC } from 'react'
import Editor, { loader } from '@monaco-editor/react'
import React, { useRef } from 'react'
import Base from '../base'
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
loader.config({ paths: { vs: '/vs' } })
type Props = {
export type Props = {
value?: string | object
placeholder?: string
onChange?: (value: string) => void
title: JSX.Element
language: CodeLanguage
@@ -19,6 +21,7 @@ type Props = {
isJSONStringifyBeauty?: boolean
height?: number
isInNode?: boolean
onMount?: (editor: any, monaco: any) => void
}
const languageMap = {
@@ -29,6 +32,7 @@ const languageMap = {
const CodeEditor: FC<Props> = ({
value = '',
placeholder = '',
onChange = () => { },
title,
headerRight,
@@ -37,6 +41,7 @@ const CodeEditor: FC<Props> = ({
isJSONStringifyBeauty,
height,
isInNode,
onMount,
}) => {
const [isFocus, setIsFocus] = React.useState(false)
@@ -47,6 +52,7 @@ const CodeEditor: FC<Props> = ({
const editorRef = useRef(null)
const handleEditorDidMount = (editor: any, monaco: any) => {
editorRef.current = editor
editor.onDidFocusEditorText(() => {
setIsFocus(true)
})
@@ -71,6 +77,8 @@ const CodeEditor: FC<Props> = ({
'editor.background': '#ffffff',
},
})
onMount?.(editor, monaco)
}
const outPutValue = (() => {
@@ -87,6 +95,7 @@ const CodeEditor: FC<Props> = ({
return (
<div>
<Base
className='relative'
title={title}
value={outPutValue}
headerRight={headerRight}
@@ -117,6 +126,7 @@ const CodeEditor: FC<Props> = ({
}}
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>
</div>