Co-authored-by: StyleZhang <jasonapring2015@outlook.com>tags/0.6.6
| @@ -38,6 +38,7 @@ const RunMode = memo(() => { | |||
| const { | |||
| doSyncWorkflowDraft, | |||
| } = useNodesSyncDraft() | |||
| const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() | |||
| const workflowRunningData = useStore(s => s.workflowRunningData) | |||
| const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running | |||
| @@ -55,10 +56,16 @@ const RunMode = memo(() => { | |||
| const startVariables = startNode?.data.variables || [] | |||
| const fileSettings = featuresStore!.getState().features.file | |||
| const { | |||
| showDebugAndPreviewPanel, | |||
| setShowDebugAndPreviewPanel, | |||
| setShowInputsPanel, | |||
| } = workflowStore.getState() | |||
| if (showDebugAndPreviewPanel) { | |||
| handleCancelDebugAndPreviewPanel() | |||
| return | |||
| } | |||
| if (!startVariables.length && !fileSettings?.image?.enabled) { | |||
| await doSyncWorkflowDraft() | |||
| handleRun({ inputs: {}, files: [] }) | |||
| @@ -75,6 +82,7 @@ const RunMode = memo(() => { | |||
| doSyncWorkflowDraft, | |||
| store, | |||
| featuresStore, | |||
| handleCancelDebugAndPreviewPanel, | |||
| ]) | |||
| return ( | |||
| @@ -21,6 +21,7 @@ export const useWorkflowInteractions = () => { | |||
| const handleCancelDebugAndPreviewPanel = useCallback(() => { | |||
| workflowStore.setState({ | |||
| showDebugAndPreviewPanel: false, | |||
| workflowRunningData: undefined, | |||
| }) | |||
| handleNodeCancelRunningStatus() | |||
| 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' | |||
| 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> | |||
| @@ -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 Field from '@/app/components/workflow/nodes/_base/components/field' | |||
| 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 { HelpCircle } from '@/app/components/base/icons/src/vender/line/general' | |||
| import type { NodePanelProps } from '@/app/components/workflow/types' | |||
| @@ -28,6 +28,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({ | |||
| inputs, | |||
| handleVarListChange, | |||
| handleAddVariable, | |||
| handleAddEmptyVariable, | |||
| handleCodeChange, | |||
| filterVar, | |||
| // single run | |||
| @@ -49,7 +50,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({ | |||
| <Field | |||
| title={t(`${i18nPrefix}.inputVars`)} | |||
| operations={ | |||
| !readOnly ? <AddButton onClick={handleAddVariable} /> : undefined | |||
| !readOnly ? <AddButton onClick={handleAddEmptyVariable} /> : undefined | |||
| } | |||
| > | |||
| <VarList | |||
| @@ -62,6 +63,9 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({ | |||
| </Field> | |||
| <Split /> | |||
| <CodeEditor | |||
| nodeId={id} | |||
| varList={inputs.variables} | |||
| onAddVar={handleAddVariable} | |||
| isInNode | |||
| readOnly={readOnly} | |||
| language={CodeLanguage.python3} | |||
| @@ -1,7 +1,7 @@ | |||
| import { useCallback, useEffect } from 'react' | |||
| import { useCallback, useEffect, useRef } from 'react' | |||
| import produce from 'immer' | |||
| import useVarList from '../_base/hooks/use-var-list' | |||
| import type { Var } from '../../types' | |||
| import type { Var, Variable } from '../../types' | |||
| import { VarType } from '../../types' | |||
| import { useStore } from '../../store' | |||
| import type { TemplateTransformNodeType } from './types' | |||
| @@ -15,12 +15,25 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { | |||
| const { nodesReadOnly: readOnly } = useNodesReadOnly() | |||
| const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type] | |||
| const { inputs, setInputs } = useNodeCrud<TemplateTransformNodeType>(id, payload) | |||
| const { handleVarListChange, handleAddVariable } = useVarList<TemplateTransformNodeType>({ | |||
| const { inputs, setInputs: doSetInputs } = useNodeCrud<TemplateTransformNodeType>(id, payload) | |||
| const inputsRef = useRef(inputs) | |||
| const setInputs = useCallback((newPayload: TemplateTransformNodeType) => { | |||
| doSetInputs(newPayload) | |||
| inputsRef.current = newPayload | |||
| }, [doSetInputs]) | |||
| const { handleVarListChange, handleAddVariable: handleAddEmptyVariable } = useVarList<TemplateTransformNodeType>({ | |||
| inputs, | |||
| setInputs, | |||
| }) | |||
| const handleAddVariable = useCallback((payload: Variable) => { | |||
| const newInputs = produce(inputsRef.current, (draft: any) => { | |||
| draft.variables.push(payload) | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [setInputs]) | |||
| useEffect(() => { | |||
| if (inputs.template) | |||
| return | |||
| @@ -36,11 +49,11 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { | |||
| }, [defaultConfig]) | |||
| const handleCodeChange = useCallback((template: string) => { | |||
| const newInputs = produce(inputs, (draft: any) => { | |||
| const newInputs = produce(inputsRef.current, (draft: any) => { | |||
| draft.template = template | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| }, [setInputs]) | |||
| // single run | |||
| const { | |||
| @@ -82,6 +95,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { | |||
| inputs, | |||
| handleVarListChange, | |||
| handleAddVariable, | |||
| handleAddEmptyVariable, | |||
| handleCodeChange, | |||
| filterVar, | |||
| // single run | |||
| @@ -49,6 +49,7 @@ const translation = { | |||
| processData: 'Process Data', | |||
| input: 'Input', | |||
| output: 'Output', | |||
| jinjaEditorPlaceholder: 'Type \'/\' or \'{\' to insert variable', | |||
| viewOnly: 'View Only', | |||
| showRunHistory: 'Show Run History', | |||
| }, | |||
| @@ -49,6 +49,7 @@ const translation = { | |||
| processData: '数据处理', | |||
| input: '输入', | |||
| output: '输出', | |||
| jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量', | |||
| viewOnly: '只读', | |||
| showRunHistory: '显示运行历史', | |||
| }, | |||