From 3891c0a2553c80ef52bc6b107ffd3bdc2e20d534 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:34:25 +0800 Subject: [PATCH] fix(workflow): correct env variable picker validation (#34666) --- .../var-reference-picker.branches.spec.tsx | 23 +++++++++++++++++++ .../var-reference-picker.helpers.spec.ts | 19 ++++++++++++--- .../variable/var-reference-picker.helpers.ts | 19 ++++++++++----- .../variable/var-reference-picker.trigger.tsx | 4 +++- .../variable/var-reference-picker.tsx | 8 +++++-- 5 files changed, 61 insertions(+), 12 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.branches.spec.tsx b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.branches.spec.tsx index 2e801c75ca..6f35e5cae7 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.branches.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.branches.spec.tsx @@ -93,6 +93,9 @@ describe('VarReferencePicker branches', () => { nodes: [startNode, sourceNode, currentNode], edges: [], hooksStoreProps: {}, + initialStoreState: { + isWorkflowDataLoaded: true, + }, }, ) @@ -222,5 +225,25 @@ describe('VarReferencePicker branches', () => { }) expect(screen.getByText('answer')).toBeInTheDocument() + expect(screen.getByTestId('var-reference-picker-error-icon')).toBeInTheDocument() + }) + + it('should not show an error icon for env variables that still exist in the workflow env list', () => { + const envVars: NodeOutPutVar[] = [ + ...availableVars, + { + nodeId: 'env', + title: 'ENVIRONMENT', + vars: [{ variable: 'env.API_KEY', type: VarType.string }], + }, + ] + + renderPicker({ + availableVars: envVars, + value: ['env', 'API_KEY'], + }) + + expect(screen.getByText('API_KEY')).toBeInTheDocument() + expect(screen.queryByTestId('var-reference-picker-error-icon')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.helpers.spec.ts b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.helpers.spec.ts index 7e86a976e6..7cef3ddde4 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.helpers.spec.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.helpers.spec.ts @@ -1,5 +1,5 @@ import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' -import type { CommonNodeType, Node, ValueSelector } from '@/app/components/workflow/types' +import type { CommonNodeType, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { createLoopNode, createNode, createStartNode } from '@/app/components/workflow/__tests__/fixtures' import { BlockEnum, VarType } from '@/app/components/workflow/types' @@ -114,11 +114,24 @@ describe('var-reference-picker.helpers', () => { }) it('should derive variable meta and category from selectors', () => { - const meta = getVariableMeta({ type: BlockEnum.Code }, ['env', 'API_KEY'], 'API_KEY') + const envVars: NodeOutPutVar[] = [{ + nodeId: 'env', + title: 'ENVIRONMENT', + vars: [{ variable: 'env.API_KEY', type: VarType.string }], + }] + const meta = getVariableMeta(null, ['env', 'API_KEY'], 'API_KEY', envVars, true) expect(meta).toMatchObject({ isEnv: true, isValidVar: true, - isException: true, + isException: false, + }) + expect(getVariableMeta(null, ['env', 'MISSING_KEY'], 'MISSING_KEY', envVars, true)).toMatchObject({ + isEnv: true, + isValidVar: false, + }) + expect(getVariableMeta(null, ['env', 'MISSING_KEY'], 'MISSING_KEY', envVars)).toMatchObject({ + isEnv: true, + isValidVar: true, }) expect(getVariableCategory({ diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.helpers.ts b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.helpers.ts index 581bdfe3df..ae076626f8 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.helpers.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.helpers.ts @@ -2,7 +2,7 @@ import type { VarType as VarKindType } from '../../../tool/types' import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations' -import type { CommonNodeType, Node, ValueSelector } from '@/app/components/workflow/types' +import type { CommonNodeType, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types' import { VAR_SHOW_NAME_MAP } from '@/app/components/workflow/constants' import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from './utils' @@ -116,13 +116,20 @@ export const getVariableMeta = ( outputVarNode: { type?: string } | null, value: ValueSelector | string, varName: string, + availableVars: NodeOutPutVar[] = [], + canValidateSpecialVars = false, ) => { const selector = value as ValueSelector - const isEnv = isENV(selector) - const isChatVar = isConversationVar(selector) - const isGlobal = isGlobalVar(selector) - const isRagVar = isRagVariableVar(selector) - const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isGlobal || isRagVar + const isSelectorValue = Array.isArray(selector) + const isEnv = isSelectorValue && isENV(selector) + const isChatVar = isSelectorValue && isConversationVar(selector) + const isGlobal = isSelectorValue && isGlobalVar(selector) + const isRagVar = isSelectorValue && isRagVariableVar(selector) + const isSpecialVar = isEnv || isChatVar || isRagVar + const hasAvailableSpecialVar = !canValidateSpecialVars || !isSelectorValue || availableVars.some(nodeWithVars => + nodeWithVars.vars.some(variable => variable.variable === selector.join('.')), + ) + const isValidVar = Boolean(outputVarNode) || isGlobal || (isSpecialVar && hasAvailableSpecialVar) return { isChatVar, isEnv, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx index 9dfa71c825..60d9c6f015 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx @@ -53,6 +53,7 @@ type Props = { schemaWithDynamicSelect?: Partial setControlFocus: (value: number) => void setOpen: (value: boolean) => void + showErrorIcon?: boolean tooltipPopup: ReactNode triggerRef: React.RefObject type?: string @@ -98,6 +99,7 @@ const VarReferencePickerTrigger: FC = ({ schemaWithDynamicSelect, setControlFocus, setOpen, + showErrorIcon = false, tooltipPopup, triggerRef, type, @@ -250,7 +252,7 @@ const VarReferencePickerTrigger: FC = ({ > {type} - {!('title' in (outputVarNode || {})) && } + {showErrorIcon && } ) : ( diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 498e4cc9a6..015bdf63b6 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -26,6 +26,7 @@ import { } from '@/app/components/workflow/hooks' // import type { BaseResource, BaseResourceProvider } from '@/app/components/workflow/nodes/_base/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' +import { useStore as useWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' import { isExceptionVariable } from '@/app/components/workflow/utils' import { useFetchDynamicOptions } from '@/service/use-plugins' @@ -124,6 +125,7 @@ const VarReferencePicker: FC = ({ const store = useStoreApi() const nodes = useNodes() const isChatMode = useIsChatMode() + const isWorkflowDataLoaded = useWorkflowStore(s => s.isWorkflowDataLoaded) const { getCurrentVariableType } = useWorkflowVariables() const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, { onlyLeafNodeVar, @@ -270,13 +272,14 @@ const VarReferencePicker: FC = ({ }) const { isEnv, isChatVar, isGlobal, isRagVar, isValidVar } = useMemo( - () => getVariableMeta(outputVarNode, value, varName), - [outputVarNode, value, varName], + () => getVariableMeta(outputVarNode, value, varName, outputVars, isWorkflowDataLoaded), + [isWorkflowDataLoaded, outputVarNode, outputVars, value, varName], ) const isException = useMemo( () => isExceptionVariable(varName, outputVarNode?.type), [outputVarNode?.type, varName], ) + const showErrorIcon = hasValue && !isValidVar // 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff const { @@ -385,6 +388,7 @@ const VarReferencePicker: FC = ({ schemaWithDynamicSelect={schemaWithDynamicSelect} setControlFocus={setControlFocus} setOpen={setOpen} + showErrorIcon={showErrorIcon} tooltipPopup={tooltipPopup} triggerRef={triggerRef} type={type}