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
pull/21802/head
Mminamiyama 10 months ago
parent bcf560870f
commit 7bdf234ab3

@ -1,211 +1,215 @@
import { import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,
} from 'lexical' } from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { import {
RiErrorWarningFill, RiErrorWarningFill,
RiMoreLine, RiMoreLine,
} from '@remixicon/react' } from '@remixicon/react'
import { useReactFlow, useStoreApi } from 'reactflow' import { useReactFlow, useStoreApi } from 'reactflow'
import { useSelectOrDelete } from '../../hooks' import { useSelectOrDelete } from '../../hooks'
import type { WorkflowNodesMap } from './node' import type { WorkflowNodesMap } from './node'
import { WorkflowVariableBlockNode } from './node' import { WorkflowVariableBlockNode } from './node'
import { import {
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
UPDATE_WORKFLOW_NODES_MAP, UPDATE_WORKFLOW_NODES_MAP,
} from './index' } from './index'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common' import { Line3 } from '@/app/components/base/icons/src/public/common'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { isExceptionVariable } from '@/app/components/workflow/utils' import { isExceptionVariable } from '@/app/components/workflow/utils'
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel' 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 } from '@/app/components/workflow/nodes/llm/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types' import type { ValueSelector, Var } from '@/app/components/workflow/types'
type WorkflowVariableBlockComponentProps = { type WorkflowVariableBlockComponentProps = {
nodeKey: string nodeKey: string
variables: string[] variables: string[]
workflowNodesMap: WorkflowNodesMap workflowNodesMap: WorkflowNodesMap
environmentVariables?: Var[] environmentVariables?: Var[]
conversationVariables?: Var[] conversationVariables?: Var[]
getVarType?: (payload: { getVarType?: (payload: {
nodeId: string, nodeId: string,
valueSelector: ValueSelector, valueSelector: ValueSelector,
}) => Type }) => Type
} }
const WorkflowVariableBlockComponent = ({ const WorkflowVariableBlockComponent = ({
nodeKey, nodeKey,
variables, variables,
workflowNodesMap = {}, workflowNodesMap = {},
getVarType, getVarType,
environmentVariables = [], environmentVariables,
conversationVariables = [], conversationVariables,
}: WorkflowVariableBlockComponentProps) => { }: WorkflowVariableBlockComponentProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND) const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
const variablesLength = variables.length const variablesLength = variables.length
const isShowAPart = variablesLength > 2 const isShowAPart = variablesLength > 2
const varName = ( const varName = (
() => { () => {
const isSystem = isSystemVar(variables) const isSystem = isSystemVar(variables)
const varName = variables[variablesLength - 1] const varName = variables[variablesLength - 1]
return `${isSystem ? 'sys.' : ''}${varName}` return `${isSystem ? 'sys.' : ''}${varName}`
} }
)() )()
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap) const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
const node = localWorkflowNodesMap![variables[0]] const node = localWorkflowNodesMap![variables[0]]
const isEnv = isENV(variables) const isEnv = isENV(variables)
const isChatVar = isConversationVar(variables) const isChatVar = isConversationVar(variables)
const isException = isExceptionVariable(varName, node?.type) const isException = isExceptionVariable(varName, node?.type)
let variableValid = true let variableValid = true
if (isEnv) if (isEnv) {
variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) if (environmentVariables)
variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
else if (isChatVar) }
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) else if (isChatVar) {
if (conversationVariables)
else variableValid = !!node variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
}
const reactflow = useReactFlow() else {
const store = useStoreApi() variableValid = !!node
}
useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode])) const reactflow = useReactFlow()
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor') const store = useStoreApi()
return mergeRegister( useEffect(() => {
editor.registerCommand( if (!editor.hasNodes([WorkflowVariableBlockNode]))
UPDATE_WORKFLOW_NODES_MAP, throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
(workflowNodesMap: WorkflowNodesMap) => {
setLocalWorkflowNodesMap(workflowNodesMap) return mergeRegister(
editor.registerCommand(
return true UPDATE_WORKFLOW_NODES_MAP,
}, (workflowNodesMap: WorkflowNodesMap) => {
COMMAND_PRIORITY_EDITOR, setLocalWorkflowNodesMap(workflowNodesMap)
),
) return true
}, [editor]) },
COMMAND_PRIORITY_EDITOR,
const handleVariableJump = useCallback(() => { ),
const workflowContainer = document.getElementById('workflow-container') )
const { }, [editor])
clientWidth,
clientHeight, const handleVariableJump = useCallback(() => {
} = workflowContainer! const workflowContainer = document.getElementById('workflow-container')
const {
const { clientWidth,
setViewport, clientHeight,
} = reactflow } = workflowContainer!
const { transform } = store.getState()
const zoom = transform[2] const {
const position = node.position setViewport,
setViewport({ } = reactflow
x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom, const { transform } = store.getState()
y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom, const zoom = transform[2]
zoom: transform[2], const position = node.position
}) setViewport({
}, [node, reactflow, store]) x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom,
y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom,
const Item = ( zoom: transform[2],
<div })
className={cn( }, [node, reactflow, store])
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] hover:border-state-accent-solid hover:bg-state-accent-hover',
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark', const Item = (
!variableValid && '!border-state-destructive-solid !bg-state-destructive-hover', <div
)} className={cn(
onClick={(e) => { 'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] hover:border-state-accent-solid hover:bg-state-accent-hover',
e.stopPropagation() isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
handleVariableJump() !variableValid && '!border-state-destructive-solid !bg-state-destructive-hover',
}} )}
ref={ref} onClick={(e) => {
> e.stopPropagation()
{!isEnv && !isChatVar && ( handleVariableJump()
<div className='flex items-center'> }}
{ ref={ref}
node?.type && ( >
<div className='p-[1px]'> {!isEnv && !isChatVar && (
<VarBlockIcon <div className='flex items-center'>
className='!text-text-secondary' {
type={node?.type} node?.type && (
/> <div className='p-[1px]'>
</div> <VarBlockIcon
) className='!text-text-secondary'
} type={node?.type}
<div className='mx-0.5 max-w-[60px] shrink-0 truncate text-xs font-medium text-text-secondary' title={node?.title} style={{ />
}}>{node?.title}</div> </div>
<Line3 className='mr-0.5 text-divider-deep'></Line3> )
</div> }
)} <div className='mx-0.5 max-w-[60px] shrink-0 truncate text-xs font-medium text-text-secondary' title={node?.title} style={{
{isShowAPart && ( }}>{node?.title}</div>
<div className='flex items-center'> <Line3 className='mr-0.5 text-divider-deep'></Line3>
<RiMoreLine className='h-3 w-3 text-text-secondary' /> </div>
<Line3 className='mr-0.5 text-divider-deep'></Line3> )}
</div> {isShowAPart && (
)} <div className='flex items-center'>
<RiMoreLine className='h-3 w-3 text-text-secondary' />
<div className='flex items-center text-text-accent'> <Line3 className='mr-0.5 text-divider-deep'></Line3>
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0', isException && 'text-text-warning')} />} </div>
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />} )}
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
<div className={cn( <div className='flex items-center text-text-accent'>
'ml-0.5 shrink-0 truncate text-xs font-medium', {!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0', isException && 'text-text-warning')} />}
isEnv && 'text-util-colors-violet-violet-600', {isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
isChatVar && 'text-util-colors-teal-teal-700', {isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
isException && 'text-text-warning', <div className={cn(
)} title={varName}>{varName}</div> 'ml-0.5 shrink-0 truncate text-xs font-medium',
{ isEnv && 'text-util-colors-violet-violet-600',
!variableValid && ( isChatVar && 'text-util-colors-teal-teal-700',
<RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' /> isException && 'text-text-warning',
) )} title={varName}>{varName}</div>
} {
</div> !variableValid && (
</div> <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' />
) )
}
if (!variableValid) { </div>
return ( </div>
<Tooltip popupContent={t('workflow.errorMsg.invalidVariable')}> )
{Item}
</Tooltip> if (!variableValid) {
) return (
} <Tooltip popupContent={t('workflow.errorMsg.invalidVariable')}>
{Item}
if (!node) </Tooltip>
return Item )
}
return (
<Tooltip if (!node)
noDecoration return Item
popupContent={
<VarFullPathPanel return (
nodeName={node.title} <Tooltip
path={variables.slice(1)} noDecoration
varType={getVarType ? getVarType({ popupContent={
nodeId: variables[0], <VarFullPathPanel
valueSelector: variables, nodeName={node.title}
}) : Type.string} path={variables.slice(1)}
nodeType={node?.type} varType={getVarType ? getVarType({
/>} nodeId: variables[0],
disabled={!isShowAPart} valueSelector: variables,
> }) : Type.string}
<div>{Item}</div> nodeType={node?.type}
</Tooltip> />}
) disabled={!isShowAPart}
} >
<div>{Item}</div>
export default memo(WorkflowVariableBlockComponent) </Tooltip>
)
}
export default memo(WorkflowVariableBlockComponent)

@ -1,67 +1,67 @@
import { import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
} from 'react' } from 'react'
import type { TextNode } from 'lexical' import type { TextNode } from 'lexical'
import { $applyNodeReplacement } from 'lexical' import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils' import { decoratorTransform } from '../../utils'
import type { WorkflowVariableBlockType } from '../../types' import type { WorkflowVariableBlockType } from '../../types'
import { CustomTextNode } from '../custom-text/node' import { CustomTextNode } from '../custom-text/node'
import { $createWorkflowVariableBlockNode } from './node' import { $createWorkflowVariableBlockNode } from './node'
import { WorkflowVariableBlockNode } from './index' import { WorkflowVariableBlockNode } from './index'
import { VAR_REGEX as REGEX, resetReg } from '@/config' import { VAR_REGEX as REGEX, resetReg } from '@/config'
const WorkflowVariableBlockReplacementBlock = ({ const WorkflowVariableBlockReplacementBlock = ({
workflowNodesMap, workflowNodesMap,
getVarType, getVarType,
onInsert, onInsert,
variables, variables,
}: WorkflowVariableBlockType) => { }: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
useEffect(() => { useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode])) if (!editor.hasNodes([WorkflowVariableBlockNode]))
throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor') throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor')
}, [editor]) }, [editor])
const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => { const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => {
if (onInsert) if (onInsert)
onInsert() onInsert()
const nodePathString = textNode.getTextContent().slice(3, -3) 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)) 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]) }, [onInsert, workflowNodesMap, getVarType, variables])
const getMatch = useCallback((text: string) => { const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text) const matchArr = REGEX.exec(text)
if (matchArr === null) if (matchArr === null)
return null return null
const startOffset = matchArr.index const startOffset = matchArr.index
const endOffset = startOffset + matchArr[0].length const endOffset = startOffset + matchArr[0].length
return { return {
end: endOffset, end: endOffset,
start: startOffset, start: startOffset,
} }
}, []) }, [])
const transformListener = useCallback((textNode: CustomTextNode) => { const transformListener = useCallback((textNode: CustomTextNode) => {
resetReg() resetReg()
return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode) return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
}, [createWorkflowVariableBlockNode, getMatch]) }, [createWorkflowVariableBlockNode, getMatch])
useEffect(() => { useEffect(() => {
resetReg() resetReg()
return mergeRegister( return mergeRegister(
editor.registerNodeTransform(CustomTextNode, transformListener), editor.registerNodeTransform(CustomTextNode, transformListener),
) )
}, []) }, [])
return null return null
} }
export default memo(WorkflowVariableBlockReplacementBlock) export default memo(WorkflowVariableBlockReplacementBlock)

Loading…
Cancel
Save