From 7bdf234ab3e30f1228be269edce64f9040364023 Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Mon, 14 Jul 2025 06:44:57 +0800 Subject: [PATCH] fix(workflow-variable-block): handle undefined environment and conversation variables Add null checks for environmentVariables and conversationVariables in variable validation logic to prevent runtime errors when these props are undefined --- .../workflow-variable-block/component.tsx | 426 +++++++++--------- ...kflow-variable-block-replacement-block.tsx | 134 +++--- 2 files changed, 282 insertions(+), 278 deletions(-) diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index fa5f99dae5..a617fb591e 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -1,211 +1,215 @@ -import { - memo, - useCallback, - useEffect, - useState, -} from 'react' -import { useTranslation } from 'react-i18next' -import { - COMMAND_PRIORITY_EDITOR, -} from 'lexical' -import { mergeRegister } from '@lexical/utils' -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { - RiErrorWarningFill, - RiMoreLine, -} from '@remixicon/react' -import { useReactFlow, useStoreApi } from 'reactflow' -import { useSelectOrDelete } from '../../hooks' -import type { WorkflowNodesMap } from './node' -import { WorkflowVariableBlockNode } from './node' -import { - DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, - UPDATE_WORKFLOW_NODES_MAP, -} from './index' -import cn from '@/utils/classnames' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' -import { VarBlockIcon } from '@/app/components/workflow/block-icon' -import { Line3 } from '@/app/components/base/icons/src/public/common' -import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' -import Tooltip from '@/app/components/base/tooltip' -import { isExceptionVariable } from '@/app/components/workflow/utils' -import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel' -import { Type } from '@/app/components/workflow/nodes/llm/types' -import type { ValueSelector, Var } from '@/app/components/workflow/types' - -type WorkflowVariableBlockComponentProps = { - nodeKey: string - variables: string[] - workflowNodesMap: WorkflowNodesMap - environmentVariables?: Var[] - conversationVariables?: Var[] - getVarType?: (payload: { - nodeId: string, - valueSelector: ValueSelector, - }) => Type -} - -const WorkflowVariableBlockComponent = ({ - nodeKey, - variables, - workflowNodesMap = {}, - getVarType, - environmentVariables = [], - conversationVariables = [], -}: WorkflowVariableBlockComponentProps) => { - const { t } = useTranslation() - const [editor] = useLexicalComposerContext() - const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND) - const variablesLength = variables.length - const isShowAPart = variablesLength > 2 - const varName = ( - () => { - const isSystem = isSystemVar(variables) - const varName = variables[variablesLength - 1] - return `${isSystem ? 'sys.' : ''}${varName}` - } - )() - const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState(workflowNodesMap) - const node = localWorkflowNodesMap![variables[0]] - const isEnv = isENV(variables) - const isChatVar = isConversationVar(variables) - const isException = isExceptionVariable(varName, node?.type) - - let variableValid = true - if (isEnv) - variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) - - else if (isChatVar) - variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) - - else variableValid = !!node - - const reactflow = useReactFlow() - const store = useStoreApi() - - useEffect(() => { - if (!editor.hasNodes([WorkflowVariableBlockNode])) - throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor') - - return mergeRegister( - editor.registerCommand( - UPDATE_WORKFLOW_NODES_MAP, - (workflowNodesMap: WorkflowNodesMap) => { - setLocalWorkflowNodesMap(workflowNodesMap) - - return true - }, - COMMAND_PRIORITY_EDITOR, - ), - ) - }, [editor]) - - const handleVariableJump = useCallback(() => { - const workflowContainer = document.getElementById('workflow-container') - const { - clientWidth, - clientHeight, - } = workflowContainer! - - const { - setViewport, - } = reactflow - const { transform } = store.getState() - const zoom = transform[2] - const position = node.position - setViewport({ - x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom, - y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom, - zoom: transform[2], - }) - }, [node, reactflow, store]) - - const Item = ( -
{ - e.stopPropagation() - handleVariableJump() - }} - ref={ref} - > - {!isEnv && !isChatVar && ( -
- { - node?.type && ( -
- -
- ) - } -
{node?.title}
- -
- )} - {isShowAPart && ( -
- - -
- )} - -
- {!isEnv && !isChatVar && } - {isEnv && } - {isChatVar && } -
{varName}
- { - !variableValid && ( - - ) - } -
-
- ) - - if (!variableValid) { - return ( - - {Item} - - ) - } - - if (!node) - return Item - - return ( - } - disabled={!isShowAPart} - > -
{Item}
-
- ) -} - -export default memo(WorkflowVariableBlockComponent) +import { + memo, + useCallback, + useEffect, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { + COMMAND_PRIORITY_EDITOR, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { + RiErrorWarningFill, + RiMoreLine, +} from '@remixicon/react' +import { useReactFlow, useStoreApi } from 'reactflow' +import { useSelectOrDelete } from '../../hooks' +import type { WorkflowNodesMap } from './node' +import { WorkflowVariableBlockNode } from './node' +import { + DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, + UPDATE_WORKFLOW_NODES_MAP, +} from './index' +import cn from '@/utils/classnames' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' +import { VarBlockIcon } from '@/app/components/workflow/block-icon' +import { Line3 } from '@/app/components/base/icons/src/public/common' +import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' +import Tooltip from '@/app/components/base/tooltip' +import { isExceptionVariable } from '@/app/components/workflow/utils' +import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel' +import { Type } from '@/app/components/workflow/nodes/llm/types' +import type { ValueSelector, Var } from '@/app/components/workflow/types' + +type WorkflowVariableBlockComponentProps = { + nodeKey: string + variables: string[] + workflowNodesMap: WorkflowNodesMap + environmentVariables?: Var[] + conversationVariables?: Var[] + getVarType?: (payload: { + nodeId: string, + valueSelector: ValueSelector, + }) => Type +} + +const WorkflowVariableBlockComponent = ({ + nodeKey, + variables, + workflowNodesMap = {}, + getVarType, + environmentVariables, + conversationVariables, +}: WorkflowVariableBlockComponentProps) => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND) + const variablesLength = variables.length + const isShowAPart = variablesLength > 2 + const varName = ( + () => { + const isSystem = isSystemVar(variables) + const varName = variables[variablesLength - 1] + return `${isSystem ? 'sys.' : ''}${varName}` + } + )() + const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState(workflowNodesMap) + const node = localWorkflowNodesMap![variables[0]] + const isEnv = isENV(variables) + const isChatVar = isConversationVar(variables) + const isException = isExceptionVariable(varName, node?.type) + + let variableValid = true + if (isEnv) { + if (environmentVariables) + variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) + } + else if (isChatVar) { + if (conversationVariables) + variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) + } + else { + variableValid = !!node + } + + const reactflow = useReactFlow() + const store = useStoreApi() + + useEffect(() => { + if (!editor.hasNodes([WorkflowVariableBlockNode])) + throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + UPDATE_WORKFLOW_NODES_MAP, + (workflowNodesMap: WorkflowNodesMap) => { + setLocalWorkflowNodesMap(workflowNodesMap) + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor]) + + const handleVariableJump = useCallback(() => { + const workflowContainer = document.getElementById('workflow-container') + const { + clientWidth, + clientHeight, + } = workflowContainer! + + const { + setViewport, + } = reactflow + const { transform } = store.getState() + const zoom = transform[2] + const position = node.position + setViewport({ + x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom, + y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom, + zoom: transform[2], + }) + }, [node, reactflow, store]) + + const Item = ( +
{ + e.stopPropagation() + handleVariableJump() + }} + ref={ref} + > + {!isEnv && !isChatVar && ( +
+ { + node?.type && ( +
+ +
+ ) + } +
{node?.title}
+ +
+ )} + {isShowAPart && ( +
+ + +
+ )} + +
+ {!isEnv && !isChatVar && } + {isEnv && } + {isChatVar && } +
{varName}
+ { + !variableValid && ( + + ) + } +
+
+ ) + + if (!variableValid) { + return ( + + {Item} + + ) + } + + if (!node) + return Item + + return ( + } + disabled={!isShowAPart} + > +
{Item}
+
+ ) +} + +export default memo(WorkflowVariableBlockComponent) diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx index 8036819ba5..5a19f50cf5 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx @@ -1,67 +1,67 @@ -import { - memo, - useCallback, - useEffect, -} from 'react' -import type { TextNode } from 'lexical' -import { $applyNodeReplacement } from 'lexical' -import { mergeRegister } from '@lexical/utils' -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { decoratorTransform } from '../../utils' -import type { WorkflowVariableBlockType } from '../../types' -import { CustomTextNode } from '../custom-text/node' -import { $createWorkflowVariableBlockNode } from './node' -import { WorkflowVariableBlockNode } from './index' -import { VAR_REGEX as REGEX, resetReg } from '@/config' - -const WorkflowVariableBlockReplacementBlock = ({ - workflowNodesMap, - getVarType, - onInsert, - variables, -}: WorkflowVariableBlockType) => { - const [editor] = useLexicalComposerContext() - - useEffect(() => { - if (!editor.hasNodes([WorkflowVariableBlockNode])) - throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor') - }, [editor]) - - const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => { - if (onInsert) - onInsert() - - const nodePathString = textNode.getTextContent().slice(3, -3) - return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables?.find(o => o.nodeId === 'env')?.vars, variables?.find(o => o.nodeId === 'conversation')?.vars)) - }, [onInsert, workflowNodesMap, getVarType, variables]) - - const getMatch = useCallback((text: string) => { - const matchArr = REGEX.exec(text) - - if (matchArr === null) - return null - - const startOffset = matchArr.index - const endOffset = startOffset + matchArr[0].length - return { - end: endOffset, - start: startOffset, - } - }, []) - - const transformListener = useCallback((textNode: CustomTextNode) => { - resetReg() - return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode) - }, [createWorkflowVariableBlockNode, getMatch]) - - useEffect(() => { - resetReg() - return mergeRegister( - editor.registerNodeTransform(CustomTextNode, transformListener), - ) - }, []) - - return null -} - -export default memo(WorkflowVariableBlockReplacementBlock) +import { + memo, + useCallback, + useEffect, +} from 'react' +import type { TextNode } from 'lexical' +import { $applyNodeReplacement } from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { decoratorTransform } from '../../utils' +import type { WorkflowVariableBlockType } from '../../types' +import { CustomTextNode } from '../custom-text/node' +import { $createWorkflowVariableBlockNode } from './node' +import { WorkflowVariableBlockNode } from './index' +import { VAR_REGEX as REGEX, resetReg } from '@/config' + +const WorkflowVariableBlockReplacementBlock = ({ + workflowNodesMap, + getVarType, + onInsert, + variables, +}: WorkflowVariableBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([WorkflowVariableBlockNode])) + throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor') + }, [editor]) + + const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => { + if (onInsert) + onInsert() + + const nodePathString = textNode.getTextContent().slice(3, -3) + return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables?.find(o => o.nodeId === 'env')?.vars || [], variables?.find(o => o.nodeId === 'conversation')?.vars || [])) + }, [onInsert, workflowNodesMap, getVarType, variables]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + matchArr[0].length + return { + end: endOffset, + start: startOffset, + } + }, []) + + const transformListener = useCallback((textNode: CustomTextNode) => { + resetReg() + return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode) + }, [createWorkflowVariableBlockNode, getMatch]) + + useEffect(() => { + resetReg() + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, transformListener), + ) + }, []) + + return null +} + +export default memo(WorkflowVariableBlockReplacementBlock)