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