mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-14 05:16:53 +08:00
Feature/newnew workflow loop node (#14863)
Co-authored-by: arkunzz <4873204@qq.com>
This commit is contained in:
80
web/app/components/workflow/nodes/loop/add-block.tsx
Normal file
80
web/app/components/workflow/nodes/loop/add-block.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '../../hooks'
|
||||
import type { LoopNodeType } from './types'
|
||||
import cn from '@/utils/classnames'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type AddBlockProps = {
|
||||
loopNodeId: string
|
||||
loopNodeData: LoopNodeType
|
||||
}
|
||||
const AddBlock = ({
|
||||
loopNodeData,
|
||||
}: AddBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false, true)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: loopNodeData.start_node_id,
|
||||
prevNodeSourceHandle: 'source',
|
||||
},
|
||||
)
|
||||
}, [handleNodeAdd, loopNodeData.start_node_id])
|
||||
|
||||
const renderTriggerElement = useCallback((open: boolean) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative inline-flex items-center px-3 h-8 rounded-lg border-[0.5px] border-gray-50 bg-white shadow-xs cursor-pointer hover:bg-gray-200 text-[13px] font-medium text-gray-700',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
open && '!bg-gray-50',
|
||||
)}>
|
||||
<RiAddLine className='mr-1 w-4 h-4' />
|
||||
{t('workflow.common.addBlock')}
|
||||
</div>
|
||||
)
|
||||
}, [nodesReadOnly, t])
|
||||
|
||||
return (
|
||||
<div className='absolute top-7 left-14 flex items-center h-8 z-10'>
|
||||
<div className='group/insert relative w-16 h-0.5 bg-gray-300'>
|
||||
<div className='absolute right-0 top-1/2 -translate-y-1/2 w-0.5 h-2 bg-primary-500'></div>
|
||||
</div>
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTriggerElement}
|
||||
triggerInnerClassName='inline-flex'
|
||||
popupClassName='!min-w-[256px]'
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddBlock)
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import type { HandleAddCondition } from '../types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ConditionAddProps = {
|
||||
className?: string
|
||||
variables: NodeOutPutVar[]
|
||||
onSelectVariable: HandleAddCondition
|
||||
disabled?: boolean
|
||||
}
|
||||
const ConditionAdd = ({
|
||||
className,
|
||||
variables,
|
||||
onSelectVariable,
|
||||
disabled,
|
||||
}: ConditionAddProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleSelectVariable = useCallback((valueSelector: ValueSelector, varItem: Var) => {
|
||||
onSelectVariable(valueSelector, varItem)
|
||||
setOpen(false)
|
||||
}, [onSelectVariable, setOpen])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(!open)}>
|
||||
<Button
|
||||
size='small'
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
>
|
||||
<RiAddLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.ifElse.addCondition')}
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<VarReferenceVars
|
||||
vars={variables}
|
||||
isSupportFileVar
|
||||
onChange={handleSelectVariable}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionAdd
|
||||
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ComparisonOperator, type Condition } from '../types'
|
||||
import {
|
||||
comparisonOperatorNotRequireValue,
|
||||
isComparisonOperatorNeedTranslate,
|
||||
isEmptyRelatedOperator,
|
||||
} from '../utils'
|
||||
import type { ValueSelector } from '../../../types'
|
||||
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from './../default'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import cn from '@/utils/classnames'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
type ConditionValueProps = {
|
||||
condition: Condition
|
||||
}
|
||||
const ConditionValue = ({
|
||||
condition,
|
||||
}: ConditionValueProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
variable_selector,
|
||||
comparison_operator: operator,
|
||||
sub_variable_condition,
|
||||
} = condition
|
||||
|
||||
const variableSelector = variable_selector as ValueSelector
|
||||
|
||||
const variableName = (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
|
||||
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
||||
const notHasValue = comparisonOperatorNotRequireValue(operator)
|
||||
const isEnvVar = isENV(variableSelector)
|
||||
const isChatVar = isConversationVar(variableSelector)
|
||||
const formatValue = useCallback((c: Condition) => {
|
||||
const notHasValue = comparisonOperatorNotRequireValue(c.comparison_operator)
|
||||
if (notHasValue)
|
||||
return ''
|
||||
|
||||
const value = c.value as string
|
||||
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
}, [])
|
||||
|
||||
const isSelect = useCallback((c: Condition) => {
|
||||
return c.comparison_operator === ComparisonOperator.in || c.comparison_operator === ComparisonOperator.notIn
|
||||
}, [])
|
||||
|
||||
const selectName = useCallback((c: Condition) => {
|
||||
const isSelect = c.comparison_operator === ComparisonOperator.in || c.comparison_operator === ComparisonOperator.notIn
|
||||
if (isSelect) {
|
||||
const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(c.value) ? c.value[0] : c.value))[0]
|
||||
return name
|
||||
? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
: ''
|
||||
}
|
||||
return ''
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<div className='rounded-md bg-workflow-block-parma-bg'>
|
||||
<div className='flex items-center px-1 h-6 '>
|
||||
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
|
||||
{isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 truncate text-xs font-medium text-text-accent',
|
||||
!notHasValue && 'max-w-[70px]',
|
||||
)}
|
||||
title={variableName}
|
||||
>
|
||||
{variableName}
|
||||
</div>
|
||||
<div
|
||||
className='shrink-0 mx-1 text-xs font-medium text-text-primary'
|
||||
title={operatorName}
|
||||
>
|
||||
{operatorName}
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-[10px] pl-[10px] border-l border-divider-regular'>
|
||||
{
|
||||
sub_variable_condition?.conditions.map((c: Condition, index) => (
|
||||
<div className='relative flex items-center h-6 space-x-1' key={c.id}>
|
||||
<div className='text-text-accent system-xs-medium'>{c.key}</div>
|
||||
<div className='text-text-primary system-xs-medium'>{isComparisonOperatorNeedTranslate(c.comparison_operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${c.comparison_operator}`) : c.comparison_operator}</div>
|
||||
{c.comparison_operator && !isEmptyRelatedOperator(c.comparison_operator) && <div className='text-text-secondary system-xs-regular'>{isSelect(c) ? selectName(c) : formatValue(c)}</div>}
|
||||
{index !== sub_variable_condition.conditions.length - 1 && (<div className='absolute z-10 right-1 bottom-[-10px] leading-4 text-[10px] font-medium text-text-accent uppercase'>{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}`)}</div>)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConditionValue)
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type {
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ConditionInputProps = {
|
||||
disabled?: boolean
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
availableNodes: Node[]
|
||||
}
|
||||
const ConditionInput = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
availableNodes,
|
||||
}: ConditionInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
|
||||
|
||||
return (
|
||||
<PromptEditor
|
||||
key={controlPromptEditorRerenderKey}
|
||||
compact
|
||||
value={value}
|
||||
placeholder={t('workflow.nodes.ifElse.enterValue') || ''}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
onChange={onChange}
|
||||
editable={!disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionInput
|
||||
@@ -0,0 +1,330 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import produce from 'immer'
|
||||
import type { VarType as NumberVarType } from '../../../tool/types'
|
||||
import type {
|
||||
Condition,
|
||||
HandleAddSubVariableCondition,
|
||||
HandleRemoveCondition,
|
||||
HandleToggleSubVariableConditionLogicalOperator,
|
||||
HandleUpdateCondition,
|
||||
HandleUpdateSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
} from '../../types'
|
||||
import {
|
||||
ComparisonOperator,
|
||||
} from '../../types'
|
||||
import ConditionNumberInput from '../condition-number-input'
|
||||
import ConditionWrap from '../condition-wrap'
|
||||
import { comparisonOperatorNotRequireValue, getOperators } from './../../utils'
|
||||
import ConditionOperator from './condition-operator'
|
||||
import ConditionInput from './condition-input'
|
||||
import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from './../../default'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import ConditionVarSelector from './condition-var-selector'
|
||||
|
||||
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
|
||||
|
||||
type ConditionItemProps = {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
conditionId: string // in isSubVariableKey it's the value of the parent condition's id
|
||||
condition: Condition // condition may the condition of case or condition of sub variable
|
||||
file?: { key: string }
|
||||
isSubVariableKey?: boolean
|
||||
isValueFieldShort?: boolean
|
||||
onRemoveCondition?: HandleRemoveCondition
|
||||
onUpdateCondition?: HandleUpdateCondition
|
||||
onAddSubVariableCondition?: HandleAddSubVariableCondition
|
||||
onRemoveSubVariableCondition?: handleRemoveSubVariableCondition
|
||||
onUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
|
||||
onToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
|
||||
nodeId: string
|
||||
availableNodes: Node[]
|
||||
numberVariables: NodeOutPutVar[]
|
||||
availableVars: NodeOutPutVar[]
|
||||
}
|
||||
const ConditionItem = ({
|
||||
className,
|
||||
disabled,
|
||||
conditionId,
|
||||
condition,
|
||||
file,
|
||||
isSubVariableKey,
|
||||
isValueFieldShort,
|
||||
onRemoveCondition,
|
||||
onUpdateCondition,
|
||||
onAddSubVariableCondition,
|
||||
onRemoveSubVariableCondition,
|
||||
onUpdateSubVariableCondition,
|
||||
onToggleSubVariableConditionLogicalOperator,
|
||||
nodeId,
|
||||
availableNodes,
|
||||
numberVariables,
|
||||
availableVars,
|
||||
}: ConditionItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const doUpdateCondition = useCallback((newCondition: Condition) => {
|
||||
if (isSubVariableKey)
|
||||
onUpdateSubVariableCondition?.(conditionId, condition.id, newCondition)
|
||||
else
|
||||
onUpdateCondition?.(condition.id, newCondition)
|
||||
}, [condition, conditionId, isSubVariableKey, onUpdateCondition, onUpdateSubVariableCondition])
|
||||
|
||||
const canChooseOperator = useMemo(() => {
|
||||
if (disabled)
|
||||
return false
|
||||
|
||||
if (isSubVariableKey)
|
||||
return !!condition.key
|
||||
|
||||
return true
|
||||
}, [condition.key, disabled, isSubVariableKey])
|
||||
const handleUpdateConditionOperator = useCallback((value: ComparisonOperator) => {
|
||||
const newCondition = {
|
||||
...condition,
|
||||
comparison_operator: value,
|
||||
}
|
||||
doUpdateCondition(newCondition)
|
||||
}, [condition, doUpdateCondition])
|
||||
|
||||
const handleUpdateConditionNumberVarType = useCallback((numberVarType: NumberVarType) => {
|
||||
const newCondition = {
|
||||
...condition,
|
||||
numberVarType,
|
||||
value: '',
|
||||
}
|
||||
doUpdateCondition(newCondition)
|
||||
}, [condition, doUpdateCondition])
|
||||
|
||||
const isSubVariable = condition.varType === VarType.arrayFile && [ComparisonOperator.contains, ComparisonOperator.notContains, ComparisonOperator.allOf].includes(condition.comparison_operator!)
|
||||
const fileAttr = useMemo(() => {
|
||||
if (file)
|
||||
return file
|
||||
if (isSubVariableKey) {
|
||||
return {
|
||||
key: condition.key!,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}, [condition.key, file, isSubVariableKey])
|
||||
|
||||
const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type'
|
||||
|
||||
const handleUpdateConditionValue = useCallback((value: string) => {
|
||||
if (value === condition.value || (isArrayValue && value === condition.value?.[0]))
|
||||
return
|
||||
const newCondition = {
|
||||
...condition,
|
||||
value: isArrayValue ? [value] : value,
|
||||
}
|
||||
doUpdateCondition(newCondition)
|
||||
}, [condition, doUpdateCondition, isArrayValue])
|
||||
|
||||
const isSelect = condition.comparison_operator && [ComparisonOperator.in, ComparisonOperator.notIn].includes(condition.comparison_operator)
|
||||
const selectOptions = useMemo(() => {
|
||||
if (isSelect) {
|
||||
if (fileAttr?.key === 'type' || condition.comparison_operator === ComparisonOperator.allOf) {
|
||||
return FILE_TYPE_OPTIONS.map(item => ({
|
||||
name: t(`${optionNameI18NPrefix}.${item.i18nKey}`),
|
||||
value: item.value,
|
||||
}))
|
||||
}
|
||||
if (fileAttr?.key === 'transfer_method') {
|
||||
return TRANSFER_METHOD.map(item => ({
|
||||
name: t(`${optionNameI18NPrefix}.${item.i18nKey}`),
|
||||
value: item.value,
|
||||
}))
|
||||
}
|
||||
return []
|
||||
}
|
||||
return []
|
||||
}, [condition.comparison_operator, fileAttr?.key, isSelect, t])
|
||||
|
||||
const isNotInput = isSelect || isSubVariable
|
||||
|
||||
const isSubVarSelect = isSubVariableKey
|
||||
const subVarOptions = SUB_VARIABLES.map(item => ({
|
||||
name: item,
|
||||
value: item,
|
||||
}))
|
||||
|
||||
const handleSubVarKeyChange = useCallback((key: string) => {
|
||||
const newCondition = produce(condition, (draft) => {
|
||||
draft.key = key
|
||||
if (key === 'size')
|
||||
draft.varType = VarType.number
|
||||
else
|
||||
draft.varType = VarType.string
|
||||
|
||||
draft.value = ''
|
||||
draft.comparison_operator = getOperators(undefined, { key })[0]
|
||||
})
|
||||
|
||||
onUpdateSubVariableCondition?.(conditionId, condition.id, newCondition)
|
||||
}, [condition, conditionId, onUpdateSubVariableCondition])
|
||||
|
||||
const doRemoveCondition = useCallback(() => {
|
||||
if (isSubVariableKey)
|
||||
onRemoveSubVariableCondition?.(conditionId, condition.id)
|
||||
else
|
||||
onRemoveCondition?.(condition.id)
|
||||
}, [condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
|
||||
|
||||
const handleVarChange = useCallback((valueSelector: ValueSelector, varItem: Var) => {
|
||||
const newCondition = produce(condition, (draft) => {
|
||||
draft.variable_selector = valueSelector
|
||||
draft.varType = varItem.type
|
||||
draft.value = ''
|
||||
draft.comparison_operator = getOperators(varItem.type)[0]
|
||||
})
|
||||
doUpdateCondition(newCondition)
|
||||
setOpen(false)
|
||||
}, [condition, doUpdateCondition])
|
||||
|
||||
return (
|
||||
<div className={cn('flex mb-1 last-of-type:mb-0', className)}>
|
||||
<div className={cn(
|
||||
'grow bg-components-input-bg-normal rounded-lg',
|
||||
isHovered && 'bg-state-destructive-hover',
|
||||
)}>
|
||||
<div className='flex items-center p-1'>
|
||||
<div className='grow w-0'>
|
||||
{isSubVarSelect
|
||||
? (
|
||||
<Select
|
||||
wrapperClassName='h-6'
|
||||
className='pl-0 text-xs'
|
||||
optionWrapClassName='w-[165px] max-h-none'
|
||||
defaultValue={condition.key}
|
||||
items={subVarOptions}
|
||||
onSelect={item => handleSubVarKeyChange(item.value as string)}
|
||||
renderTrigger={item => (
|
||||
item
|
||||
? <div className='flex justify-start cursor-pointer'>
|
||||
<div className='inline-flex max-w-full px-1.5 items-center h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark shadow-xs text-text-accent'>
|
||||
<Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />
|
||||
<div className='ml-0.5 truncate system-xs-medium'>{item?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
: <div className='text-left text-components-input-text-placeholder system-sm-regular'>{t('common.placeholder.select')}</div>
|
||||
)}
|
||||
hideChecked
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ConditionVarSelector
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
valueSelector={condition.variable_selector || []}
|
||||
varType={condition.varType}
|
||||
availableNodes={availableNodes}
|
||||
nodesOutputVars={availableVars}
|
||||
onChange={handleVarChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className='mx-1 w-[1px] h-3 bg-divider-regular'></div>
|
||||
<ConditionOperator
|
||||
disabled={!canChooseOperator}
|
||||
varType={condition.varType}
|
||||
value={condition.comparison_operator}
|
||||
onSelect={handleUpdateConditionOperator}
|
||||
file={fileAttr}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && (
|
||||
<div className='px-2 py-1 max-h-[100px] border-t border-t-divider-subtle overflow-y-auto'>
|
||||
<ConditionInput
|
||||
disabled={disabled}
|
||||
value={condition.value as string}
|
||||
onChange={handleUpdateConditionValue}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && (
|
||||
<div className='px-2 py-1 pt-[3px] border-t border-t-divider-subtle'>
|
||||
<ConditionNumberInput
|
||||
numberVarType={condition.numberVarType}
|
||||
onNumberVarTypeChange={handleUpdateConditionNumberVarType}
|
||||
value={condition.value as string}
|
||||
onValueChange={handleUpdateConditionValue}
|
||||
variables={numberVariables}
|
||||
isShort={isValueFieldShort}
|
||||
unit={fileAttr?.key === 'size' ? 'Byte' : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && isSelect && (
|
||||
<div className='border-t border-t-divider-subtle'>
|
||||
<Select
|
||||
wrapperClassName='h-8'
|
||||
className='px-2 text-xs rounded-t-none'
|
||||
defaultValue={isArrayValue ? (condition.value as string[])?.[0] : (condition.value as string)}
|
||||
items={selectOptions}
|
||||
onSelect={item => handleUpdateConditionValue(item.value as string)}
|
||||
hideChecked
|
||||
notClearable
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && isSubVariable && (
|
||||
<div className='p-1'>
|
||||
<ConditionWrap
|
||||
isSubVariable
|
||||
conditions={condition.sub_variable_condition?.conditions || []}
|
||||
logicalOperator={condition.sub_variable_condition?.logical_operator}
|
||||
conditionId={conditionId}
|
||||
readOnly={!!disabled}
|
||||
handleAddSubVariableCondition={onAddSubVariableCondition}
|
||||
handleRemoveSubVariableCondition={onRemoveSubVariableCondition}
|
||||
handleUpdateSubVariableCondition={onUpdateSubVariableCondition}
|
||||
handleToggleSubVariableConditionLogicalOperator={onToggleSubVariableConditionLogicalOperator}
|
||||
nodeId={nodeId}
|
||||
availableNodes={availableNodes}
|
||||
availableVars={availableVars}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center ml-1 mt-1 w-6 h-6 rounded-lg cursor-pointer hover:bg-state-destructive-hover text-text-tertiary hover:text-text-destructive'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={doRemoveCondition}
|
||||
>
|
||||
<RiDeleteBinLine className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionItem
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { getOperators, isComparisonOperatorNeedTranslate } from '../../utils'
|
||||
import type { ComparisonOperator } from '../../types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { VarType } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
type ConditionOperatorProps = {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
varType: VarType
|
||||
file?: { key: string }
|
||||
value?: string
|
||||
onSelect: (value: ComparisonOperator) => void
|
||||
}
|
||||
const ConditionOperator = ({
|
||||
className,
|
||||
disabled,
|
||||
varType,
|
||||
file,
|
||||
value,
|
||||
onSelect,
|
||||
}: ConditionOperatorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const options = useMemo(() => {
|
||||
return getOperators(varType, file).map((o) => {
|
||||
return {
|
||||
label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o,
|
||||
value: o,
|
||||
}
|
||||
})
|
||||
}, [t, varType, file])
|
||||
const selectedOption = options.find(o => Array.isArray(value) ? o.value === value[0] : o.value === value)
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<Button
|
||||
className={cn('shrink-0', !selectedOption && 'opacity-50', className)}
|
||||
size='small'
|
||||
variant='ghost'
|
||||
disabled={disabled}
|
||||
>
|
||||
{
|
||||
selectedOption
|
||||
? selectedOption.label
|
||||
: t(`${i18nPrefix}.select`)
|
||||
}
|
||||
<RiArrowDownSLine className='ml-1 w-3.5 h-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className='flex items-center px-3 py-1.5 h-7 text-[13px] font-medium text-text-secondary rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onSelect(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionOperator
|
||||
@@ -0,0 +1,58 @@
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type ConditionVarSelectorProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
valueSelector: ValueSelector
|
||||
varType: VarType
|
||||
availableNodes: Node[]
|
||||
nodesOutputVars: NodeOutPutVar[]
|
||||
onChange: (valueSelector: ValueSelector, varItem: Var) => void
|
||||
}
|
||||
|
||||
const ConditionVarSelector = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
valueSelector,
|
||||
varType,
|
||||
availableNodes,
|
||||
nodesOutputVars,
|
||||
onChange,
|
||||
}: ConditionVarSelectorProps) => {
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => onOpenChange(!open)}>
|
||||
<div className="cursor-pointer">
|
||||
<VariableTag
|
||||
valueSelector={valueSelector}
|
||||
varType={varType}
|
||||
availableNodes={availableNodes}
|
||||
isShort
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<VarReferenceVars
|
||||
vars={nodesOutputVars}
|
||||
isSupportFileVar
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionVarSelector
|
||||
@@ -0,0 +1,126 @@
|
||||
import { RiLoopLeftLine } from '@remixicon/react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import {
|
||||
type Condition,
|
||||
type HandleAddSubVariableCondition,
|
||||
type HandleRemoveCondition,
|
||||
type HandleToggleConditionLogicalOperator,
|
||||
type HandleToggleSubVariableConditionLogicalOperator,
|
||||
type HandleUpdateCondition,
|
||||
type HandleUpdateSubVariableCondition,
|
||||
LogicalOperator,
|
||||
type handleRemoveSubVariableCondition,
|
||||
} from '../../types'
|
||||
import ConditionItem from './condition-item'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ConditionListProps = {
|
||||
isSubVariable?: boolean
|
||||
disabled?: boolean
|
||||
conditionId?: string
|
||||
conditions: Condition[]
|
||||
logicalOperator?: LogicalOperator
|
||||
onRemoveCondition?: HandleRemoveCondition
|
||||
onUpdateCondition?: HandleUpdateCondition
|
||||
onToggleConditionLogicalOperator?: HandleToggleConditionLogicalOperator
|
||||
nodeId: string
|
||||
availableNodes: Node[]
|
||||
numberVariables: NodeOutPutVar[]
|
||||
onAddSubVariableCondition?: HandleAddSubVariableCondition
|
||||
onRemoveSubVariableCondition?: handleRemoveSubVariableCondition
|
||||
onUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
|
||||
onToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
|
||||
availableVars: NodeOutPutVar[]
|
||||
}
|
||||
const ConditionList = ({
|
||||
isSubVariable,
|
||||
disabled,
|
||||
conditionId,
|
||||
conditions,
|
||||
logicalOperator,
|
||||
onUpdateCondition,
|
||||
onRemoveCondition,
|
||||
onToggleConditionLogicalOperator,
|
||||
onAddSubVariableCondition,
|
||||
onRemoveSubVariableCondition,
|
||||
onUpdateSubVariableCondition,
|
||||
onToggleSubVariableConditionLogicalOperator,
|
||||
nodeId,
|
||||
availableNodes,
|
||||
numberVariables,
|
||||
availableVars,
|
||||
}: ConditionListProps) => {
|
||||
const doToggleConditionLogicalOperator = useCallback((conditionId?: string) => {
|
||||
if (isSubVariable && conditionId)
|
||||
onToggleSubVariableConditionLogicalOperator?.(conditionId)
|
||||
else
|
||||
onToggleConditionLogicalOperator?.()
|
||||
}, [isSubVariable, onToggleConditionLogicalOperator, onToggleSubVariableConditionLogicalOperator])
|
||||
|
||||
const isValueFieldShort = useMemo(() => {
|
||||
if (isSubVariable && conditions.length > 1)
|
||||
return true
|
||||
|
||||
return false
|
||||
}, [conditions.length, isSubVariable])
|
||||
const conditionItemClassName = useMemo(() => {
|
||||
if (!isSubVariable)
|
||||
return ''
|
||||
if (conditions.length < 2)
|
||||
return ''
|
||||
return logicalOperator === LogicalOperator.and ? 'pl-[51px]' : 'pl-[42px]'
|
||||
}, [conditions.length, isSubVariable, logicalOperator])
|
||||
|
||||
return (
|
||||
<div className={cn('relative', conditions.length > 1 && !isSubVariable && 'pl-[60px]')}>
|
||||
{
|
||||
conditions.length > 1 && (
|
||||
<div className={cn(
|
||||
'absolute top-0 bottom-0 left-0 w-[60px]',
|
||||
isSubVariable && logicalOperator === LogicalOperator.and && 'left-[-10px]',
|
||||
isSubVariable && logicalOperator === LogicalOperator.or && 'left-[-18px]',
|
||||
)}>
|
||||
<div className='absolute top-4 bottom-4 left-[46px] w-2.5 border border-divider-deep rounded-l-[8px] border-r-0'></div>
|
||||
<div className='absolute top-1/2 -translate-y-1/2 right-0 w-4 h-[29px] bg-components-panel-bg'></div>
|
||||
<div
|
||||
className='absolute top-1/2 right-1 -translate-y-1/2 flex items-center px-1 h-[21px] rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs bg-components-button-secondary-bg text-text-accent-secondary text-[10px] font-semibold cursor-pointer select-none'
|
||||
onClick={() => doToggleConditionLogicalOperator(conditionId)}
|
||||
>
|
||||
{logicalOperator && logicalOperator.toUpperCase()}
|
||||
<RiLoopLeftLine className='ml-0.5 w-3 h-3' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
conditions.map(condition => (
|
||||
<ConditionItem
|
||||
key={condition.id}
|
||||
className={conditionItemClassName}
|
||||
disabled={disabled}
|
||||
conditionId={isSubVariable ? conditionId! : condition.id}
|
||||
condition={condition}
|
||||
isValueFieldShort={isValueFieldShort}
|
||||
onUpdateCondition={onUpdateCondition}
|
||||
onRemoveCondition={onRemoveCondition}
|
||||
onAddSubVariableCondition={onAddSubVariableCondition}
|
||||
onRemoveSubVariableCondition={onRemoveSubVariableCondition}
|
||||
onUpdateSubVariableCondition={onUpdateSubVariableCondition}
|
||||
onToggleSubVariableConditionLogicalOperator={onToggleSubVariableConditionLogicalOperator}
|
||||
nodeId={nodeId}
|
||||
availableNodes={availableNodes}
|
||||
numberVariables={numberVariables}
|
||||
isSubVariableKey={isSubVariable}
|
||||
availableVars={availableVars}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionList
|
||||
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { VarType as NumberVarType } from '../../tool/types'
|
||||
import VariableTag from '../../_base/components/variable-tag'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { variableTransformer } from '@/app/components/workflow/utils'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
|
||||
const options = [
|
||||
NumberVarType.variable,
|
||||
NumberVarType.constant,
|
||||
]
|
||||
|
||||
type ConditionNumberInputProps = {
|
||||
numberVarType?: NumberVarType
|
||||
onNumberVarTypeChange: (v: NumberVarType) => void
|
||||
value: string
|
||||
onValueChange: (v: string) => void
|
||||
variables: NodeOutPutVar[]
|
||||
isShort?: boolean
|
||||
unit?: string
|
||||
}
|
||||
const ConditionNumberInput = ({
|
||||
numberVarType = NumberVarType.constant,
|
||||
onNumberVarTypeChange,
|
||||
value,
|
||||
onValueChange,
|
||||
variables,
|
||||
isShort,
|
||||
unit,
|
||||
}: ConditionNumberInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [numberVarTypeVisible, setNumberVarTypeVisible] = useState(false)
|
||||
const [variableSelectorVisible, setVariableSelectorVisible] = useState(false)
|
||||
const [isFocus, {
|
||||
setTrue: setFocus,
|
||||
setFalse: setBlur,
|
||||
}] = useBoolean()
|
||||
|
||||
const handleSelectVariable = useCallback((valueSelector: ValueSelector) => {
|
||||
onValueChange(variableTransformer(valueSelector) as string)
|
||||
setVariableSelectorVisible(false)
|
||||
}, [onValueChange])
|
||||
|
||||
return (
|
||||
<div className='flex items-center cursor-pointer'>
|
||||
<PortalToFollowElem
|
||||
open={numberVarTypeVisible}
|
||||
onOpenChange={setNumberVarTypeVisible}
|
||||
placement='bottom-start'
|
||||
offset={{ mainAxis: 2, crossAxis: 0 }}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setNumberVarTypeVisible(v => !v)}>
|
||||
<Button
|
||||
className='shrink-0'
|
||||
variant='ghost'
|
||||
size='small'
|
||||
>
|
||||
{capitalize(numberVarType)}
|
||||
<RiArrowDownSLine className='ml-[1px] w-3.5 h-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='p-1 w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option}
|
||||
className={cn(
|
||||
'flex items-center px-3 h-7 rounded-md hover:bg-state-base-hover cursor-pointer',
|
||||
'text-[13px] font-medium text-text-secondary',
|
||||
numberVarType === option && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
onNumberVarTypeChange(option)
|
||||
setNumberVarTypeVisible(false)
|
||||
}}
|
||||
>
|
||||
{capitalize(option)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
<div className='mx-1 w-[1px] h-4 bg-divider-regular'></div>
|
||||
<div className='grow w-0 ml-0.5'>
|
||||
{
|
||||
numberVarType === NumberVarType.variable && (
|
||||
<PortalToFollowElem
|
||||
open={variableSelectorVisible}
|
||||
onOpenChange={setVariableSelectorVisible}
|
||||
placement='bottom-start'
|
||||
offset={{ mainAxis: 2, crossAxis: 0 }}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
className='w-full'
|
||||
onClick={() => setVariableSelectorVisible(v => !v)}>
|
||||
{
|
||||
value && (
|
||||
<VariableTag
|
||||
valueSelector={variableTransformer(value) as string[]}
|
||||
varType={VarType.number}
|
||||
isShort={isShort}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!value && (
|
||||
<div className='flex items-center p-1 h-6 text-components-input-text-placeholder text-[13px]'>
|
||||
<Variable02 className='shrink-0 mr-1 w-4 h-4' />
|
||||
<div className='w-0 grow truncate'>{t('workflow.nodes.ifElse.selectVariable')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className={cn('w-[296px] pt-1 bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg', isShort && 'w-[200px]')}>
|
||||
<VarReferenceVars
|
||||
vars={variables}
|
||||
onChange={handleSelectVariable}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
{
|
||||
numberVarType === NumberVarType.constant && (
|
||||
<div className=' relative'>
|
||||
<input
|
||||
className={cn('block w-full px-2 text-[13px] text-components-input-text-filled placeholder:text-components-input-text-placeholder outline-none appearance-none bg-transparent', unit && 'pr-6')}
|
||||
type='number'
|
||||
value={value}
|
||||
onChange={e => onValueChange(e.target.value)}
|
||||
placeholder={t('workflow.nodes.ifElse.enterValue') || ''}
|
||||
onFocus={setFocus}
|
||||
onBlur={setBlur}
|
||||
/>
|
||||
{!isFocus && unit && <div className='absolute right-2 top-[50%] translate-y-[-50%] text-text-tertiary system-sm-regular'>{unit}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConditionNumberInput)
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ComparisonOperator } from '../types'
|
||||
import {
|
||||
comparisonOperatorNotRequireValue,
|
||||
isComparisonOperatorNeedTranslate,
|
||||
} from '../utils'
|
||||
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from './../default'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import cn from '@/utils/classnames'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
type ConditionValueProps = {
|
||||
variableSelector: string[]
|
||||
labelName?: string
|
||||
operator: ComparisonOperator
|
||||
value: string | string[]
|
||||
}
|
||||
const ConditionValue = ({
|
||||
variableSelector,
|
||||
labelName,
|
||||
operator,
|
||||
value,
|
||||
}: ConditionValueProps) => {
|
||||
const { t } = useTranslation()
|
||||
const variableName = labelName || (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
|
||||
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
||||
const notHasValue = comparisonOperatorNotRequireValue(operator)
|
||||
const isEnvVar = isENV(variableSelector)
|
||||
const isChatVar = isConversationVar(variableSelector)
|
||||
const formatValue = useMemo(() => {
|
||||
if (notHasValue)
|
||||
return ''
|
||||
|
||||
if (Array.isArray(value)) // transfer method
|
||||
return value[0]
|
||||
|
||||
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
}, [notHasValue, value])
|
||||
|
||||
const isSelect = operator === ComparisonOperator.in || operator === ComparisonOperator.notIn
|
||||
const selectName = useMemo(() => {
|
||||
if (isSelect) {
|
||||
const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(value) ? value[0] : value))[0]
|
||||
return name
|
||||
? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
: ''
|
||||
}
|
||||
return ''
|
||||
}, [isSelect, t, value])
|
||||
|
||||
return (
|
||||
<div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
|
||||
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
|
||||
{isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 truncate text-xs font-medium text-text-accent',
|
||||
!notHasValue && 'max-w-[70px]',
|
||||
)}
|
||||
title={variableName}
|
||||
>
|
||||
{variableName}
|
||||
</div>
|
||||
<div
|
||||
className='shrink-0 mx-1 text-xs font-medium text-text-primary'
|
||||
title={operatorName}
|
||||
>
|
||||
{operatorName}
|
||||
</div>
|
||||
{
|
||||
!notHasValue && (
|
||||
<div className='truncate text-xs text-text-secondary' title={formatValue}>{isSelect ? selectName : formatValue}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConditionValue)
|
||||
@@ -0,0 +1,149 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import type { Condition, HandleAddCondition, HandleAddSubVariableCondition, HandleRemoveCondition, HandleToggleConditionLogicalOperator, HandleToggleSubVariableConditionLogicalOperator, HandleUpdateCondition, HandleUpdateSubVariableCondition, LogicalOperator, handleRemoveSubVariableCondition } from '../types'
|
||||
import type { Node, NodeOutPutVar, Var } from '../../../types'
|
||||
import { VarType } from '../../../types'
|
||||
import { useGetAvailableVars } from '../../variable-assigner/hooks'
|
||||
import ConditionList from './condition-list'
|
||||
import ConditionAdd from './condition-add'
|
||||
import { SUB_VARIABLES } from './../default'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { PortalSelect as Select } from '@/app/components/base/select'
|
||||
|
||||
type Props = {
|
||||
isSubVariable?: boolean
|
||||
conditionId?: string
|
||||
conditions: Condition[]
|
||||
logicalOperator: LogicalOperator | undefined
|
||||
readOnly: boolean
|
||||
handleAddCondition?: HandleAddCondition
|
||||
handleRemoveCondition?: HandleRemoveCondition
|
||||
handleUpdateCondition?: HandleUpdateCondition
|
||||
handleToggleConditionLogicalOperator?: HandleToggleConditionLogicalOperator
|
||||
handleAddSubVariableCondition?: HandleAddSubVariableCondition
|
||||
handleRemoveSubVariableCondition?: handleRemoveSubVariableCondition
|
||||
handleUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
|
||||
handleToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
|
||||
nodeId: string
|
||||
availableNodes: Node[]
|
||||
availableVars: NodeOutPutVar[]
|
||||
}
|
||||
|
||||
const ConditionWrap: FC<Props> = ({
|
||||
isSubVariable,
|
||||
conditionId,
|
||||
conditions,
|
||||
logicalOperator,
|
||||
nodeId: id = '',
|
||||
readOnly,
|
||||
handleUpdateCondition,
|
||||
handleAddCondition,
|
||||
handleRemoveCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
availableNodes = [],
|
||||
availableVars = [],
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getAvailableVars = useGetAvailableVars()
|
||||
|
||||
const filterNumberVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type === VarType.number
|
||||
}, [])
|
||||
|
||||
const subVarOptions = SUB_VARIABLES.map(item => ({
|
||||
name: item,
|
||||
value: item,
|
||||
}))
|
||||
|
||||
if (!conditions)
|
||||
return <div />
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
'group relative rounded-[10px] bg-components-panel-bg',
|
||||
!isSubVariable && 'py-1 px-3 min-h-[40px] ',
|
||||
isSubVariable && 'px-1 py-2',
|
||||
)}
|
||||
>
|
||||
{
|
||||
conditions && !!conditions.length && (
|
||||
<div className='mb-2'>
|
||||
<ConditionList
|
||||
disabled={readOnly}
|
||||
conditionId={conditionId}
|
||||
conditions={conditions}
|
||||
logicalOperator={logicalOperator}
|
||||
onUpdateCondition={handleUpdateCondition}
|
||||
onRemoveCondition={handleRemoveCondition}
|
||||
onToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
nodeId={id}
|
||||
availableNodes={availableNodes}
|
||||
numberVariables={getAvailableVars(id, '', filterNumberVar)}
|
||||
onAddSubVariableCondition={handleAddSubVariableCondition}
|
||||
onRemoveSubVariableCondition={handleRemoveSubVariableCondition}
|
||||
onUpdateSubVariableCondition={handleUpdateSubVariableCondition}
|
||||
onToggleSubVariableConditionLogicalOperator={handleToggleSubVariableConditionLogicalOperator}
|
||||
isSubVariable={isSubVariable}
|
||||
availableVars={availableVars}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={cn(
|
||||
'flex items-center justify-between pr-[30px]',
|
||||
!conditions.length && !isSubVariable && 'mt-1',
|
||||
!conditions.length && isSubVariable && 'mt-2',
|
||||
conditions.length > 1 && !isSubVariable && 'ml-[60px]',
|
||||
)}>
|
||||
{isSubVariable
|
||||
? (
|
||||
<Select
|
||||
popupInnerClassName='w-[165px] max-h-none'
|
||||
onSelect={value => handleAddSubVariableCondition?.(conditionId!, value.value as string)}
|
||||
items={subVarOptions}
|
||||
value=''
|
||||
renderTrigger={() => (
|
||||
<Button
|
||||
size='small'
|
||||
disabled={readOnly}
|
||||
>
|
||||
<RiAddLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.ifElse.addSubVariable')}
|
||||
</Button>
|
||||
)}
|
||||
hideChecked
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ConditionAdd
|
||||
disabled={readOnly}
|
||||
variables={availableVars}
|
||||
onSelectVariable={handleAddCondition!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isSubVariable && (
|
||||
<div className='my-2 mx-3 h-[1px] bg-divider-subtle'></div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ConditionWrap)
|
||||
92
web/app/components/workflow/nodes/loop/default.ts
Normal file
92
web/app/components/workflow/nodes/loop/default.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { ComparisonOperator, LogicalOperator, type LoopNodeType } from './types'
|
||||
import { isEmptyRelatedOperator } from './utils'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { LOOP_NODE_MAX_COUNT } from '@/config'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<LoopNodeType> = {
|
||||
defaultValue: {
|
||||
start_node_id: '',
|
||||
break_conditions: [],
|
||||
loop_count: 10,
|
||||
_children: [],
|
||||
logical_operator: LogicalOperator.and,
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: LoopNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
if (!errorMessages && (!payload.break_conditions || payload.break_conditions.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.loop.breakCondition') })
|
||||
|
||||
payload.break_conditions!.forEach((condition) => {
|
||||
if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
if (!errorMessages && !condition.comparison_operator)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.ifElse.operator') })
|
||||
if (!errorMessages) {
|
||||
if (condition.sub_variable_condition
|
||||
&& ![ComparisonOperator.empty, ComparisonOperator.notEmpty].includes(condition.comparison_operator!)) {
|
||||
const isSet = condition.sub_variable_condition.conditions.every((c) => {
|
||||
if (!c.comparison_operator)
|
||||
return false
|
||||
|
||||
if (isEmptyRelatedOperator(c.comparison_operator!))
|
||||
return true
|
||||
|
||||
return !!c.value
|
||||
})
|
||||
|
||||
if (!isSet)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
|
||||
}
|
||||
else {
|
||||
if (!isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!errorMessages && (
|
||||
Number.isNaN(Number(payload.loop_count))
|
||||
|| !Number.isInteger(Number(payload.loop_count))
|
||||
|| payload.loop_count < 1
|
||||
|| payload.loop_count > LOOP_NODE_MAX_COUNT
|
||||
))
|
||||
errorMessages = t('workflow.nodes.loop.loopMaxCountError', { maxCount: LOOP_NODE_MAX_COUNT })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const FILE_TYPE_OPTIONS = [
|
||||
{ value: 'image', i18nKey: 'image' },
|
||||
{ value: 'document', i18nKey: 'doc' },
|
||||
{ value: 'audio', i18nKey: 'audio' },
|
||||
{ value: 'video', i18nKey: 'video' },
|
||||
]
|
||||
|
||||
export const TRANSFER_METHOD = [
|
||||
{ value: TransferMethod.local_file, i18nKey: 'localUpload' },
|
||||
{ value: TransferMethod.remote_url, i18nKey: 'url' },
|
||||
]
|
||||
|
||||
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
|
||||
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')
|
||||
|
||||
export default nodeDefault
|
||||
61
web/app/components/workflow/nodes/loop/insert-block.tsx
Normal file
61
web/app/components/workflow/nodes/loop/insert-block.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useNodesInteractions } from '../../hooks'
|
||||
import type {
|
||||
BlockEnum,
|
||||
OnSelectBlock,
|
||||
} from '../../types'
|
||||
import BlockSelector from '../../block-selector'
|
||||
|
||||
type InsertBlockProps = {
|
||||
startNodeId: string
|
||||
availableBlocksTypes: BlockEnum[]
|
||||
}
|
||||
const InsertBlock = ({
|
||||
startNodeId,
|
||||
availableBlocksTypes,
|
||||
}: InsertBlockProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleInsert = useCallback<OnSelectBlock>((nodeType, toolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType,
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
nextNodeId: startNodeId,
|
||||
nextNodeTargetHandle: 'target',
|
||||
},
|
||||
)
|
||||
}, [startNodeId, handleNodeAdd])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'nopan nodrag',
|
||||
'hidden group-hover/insert:block absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2',
|
||||
open && '!block',
|
||||
)}
|
||||
>
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
triggerClassName={() => 'hover:scale-125 transition-all'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(InsertBlock)
|
||||
61
web/app/components/workflow/nodes/loop/node.tsx
Normal file
61
web/app/components/workflow/nodes/loop/node.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
Background,
|
||||
useNodesInitialized,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import { LoopStartNodeDumb } from '../loop-start'
|
||||
import { useNodeLoopInteractions } from './use-interactions'
|
||||
import type { LoopNodeType } from './types'
|
||||
import AddBlock from './add-block'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<LoopNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { zoom } = useViewport()
|
||||
const nodesInitialized = useNodesInitialized()
|
||||
const { handleNodeLoopRerender } = useNodeLoopInteractions()
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesInitialized)
|
||||
handleNodeLoopRerender(id)
|
||||
}, [nodesInitialized, id, handleNodeLoopRerender])
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative min-w-[240px] min-h-[90px] w-full h-full rounded-2xl bg-[#F0F2F7]/90',
|
||||
)}>
|
||||
<Background
|
||||
id={`loop-background-${id}`}
|
||||
className='rounded-2xl !z-0'
|
||||
gap={[14 / zoom, 14 / zoom]}
|
||||
size={2 / zoom}
|
||||
color='#E4E5E7'
|
||||
/>
|
||||
{
|
||||
data._isCandidate && (
|
||||
<LoopStartNodeDumb />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._children!.length === 1 && (
|
||||
<AddBlock
|
||||
loopNodeId={id}
|
||||
loopNodeData={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Node)
|
||||
120
web/app/components/workflow/nodes/loop/panel.tsx
Normal file
120
web/app/components/workflow/nodes/loop/panel.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import Split from '../_base/components/split'
|
||||
import ResultPanel from '../../run/result-panel'
|
||||
import InputNumberWithSlider from '../_base/components/input-number-with-slider'
|
||||
import type { LoopNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import ConditionWrap from './components/condition-wrap'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import formatTracing from '@/app/components/workflow/run/utils/format-log'
|
||||
|
||||
import { useLogs } from '@/app/components/workflow/run/hooks'
|
||||
import { LOOP_NODE_MAX_COUNT } from '@/config'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.loop'
|
||||
|
||||
const Panel: FC<NodePanelProps<LoopNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
childrenNodeVars,
|
||||
loopChildrenNodes,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
loopRunResult,
|
||||
handleAddCondition,
|
||||
handleUpdateCondition,
|
||||
handleRemoveCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
handleUpdateLoopCount,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const nodeInfo = formatTracing(loopRunResult, t)[0]
|
||||
const logsParams = useLogs()
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div>
|
||||
<Field
|
||||
title={<div className='pl-3'>{t(`${i18nPrefix}.breakCondition`)}</div>}
|
||||
>
|
||||
<ConditionWrap
|
||||
nodeId={id}
|
||||
readOnly={readOnly}
|
||||
handleAddCondition={handleAddCondition}
|
||||
handleRemoveCondition={handleRemoveCondition}
|
||||
handleUpdateCondition={handleUpdateCondition}
|
||||
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
handleAddSubVariableCondition={handleAddSubVariableCondition}
|
||||
handleRemoveSubVariableCondition={handleRemoveSubVariableCondition}
|
||||
handleUpdateSubVariableCondition={handleUpdateSubVariableCondition}
|
||||
handleToggleSubVariableConditionLogicalOperator={handleToggleSubVariableConditionLogicalOperator}
|
||||
availableNodes={loopChildrenNodes}
|
||||
availableVars={childrenNodeVars}
|
||||
conditions={inputs.break_conditions || []}
|
||||
logicalOperator={inputs.logical_operator!}
|
||||
/>
|
||||
</Field>
|
||||
<Split />
|
||||
<div className='mt-2'>
|
||||
<Field
|
||||
title={<div className='pl-3'>{t(`${i18nPrefix}.loopMaxCount`)}</div>}
|
||||
>
|
||||
<div className='px-3 py-2'>
|
||||
<InputNumberWithSlider
|
||||
min={1}
|
||||
max={LOOP_NODE_MAX_COUNT}
|
||||
value={inputs.loop_count}
|
||||
onChange={(val) => {
|
||||
const roundedVal = Math.round(val)
|
||||
handleUpdateLoopCount(Number.isNaN(roundedVal) ? 1 : roundedVal)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
{/* Error handling for the Loop node is currently not considered. */}
|
||||
{/* <div className='px-4 py-2'>
|
||||
<Field title={t(`${i18nPrefix}.errorResponseMethod`)} >
|
||||
<Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false}>
|
||||
</Select>
|
||||
</Field>
|
||||
</div> */}
|
||||
{isShowSingleRun && (
|
||||
<BeforeRunForm
|
||||
nodeName={inputs.title}
|
||||
onHide={hideSingleRun}
|
||||
forms={[]}
|
||||
runningStatus={runningStatus}
|
||||
onRun={handleRun}
|
||||
onStop={handleStop}
|
||||
{...logsParams}
|
||||
result={
|
||||
<ResultPanel {...runResult} showSteps={false} nodeInfo={nodeInfo} {...logsParams} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
76
web/app/components/workflow/nodes/loop/types.ts
Normal file
76
web/app/components/workflow/nodes/loop/types.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { VarType as NumberVarType } from '../tool/types'
|
||||
import type {
|
||||
BlockEnum,
|
||||
CommonNodeType,
|
||||
ErrorHandleMode,
|
||||
ValueSelector,
|
||||
Var,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
export enum LogicalOperator {
|
||||
and = 'and',
|
||||
or = 'or',
|
||||
}
|
||||
|
||||
export enum ComparisonOperator {
|
||||
contains = 'contains',
|
||||
notContains = 'not contains',
|
||||
startWith = 'start with',
|
||||
endWith = 'end with',
|
||||
is = 'is',
|
||||
isNot = 'is not',
|
||||
empty = 'empty',
|
||||
notEmpty = 'not empty',
|
||||
equal = '=',
|
||||
notEqual = '≠',
|
||||
largerThan = '>',
|
||||
lessThan = '<',
|
||||
largerThanOrEqual = '≥',
|
||||
lessThanOrEqual = '≤',
|
||||
isNull = 'is null',
|
||||
isNotNull = 'is not null',
|
||||
in = 'in',
|
||||
notIn = 'not in',
|
||||
allOf = 'all of',
|
||||
exists = 'exists',
|
||||
notExists = 'not exists',
|
||||
}
|
||||
|
||||
export type Condition = {
|
||||
id: string
|
||||
varType: VarType
|
||||
variable_selector?: ValueSelector
|
||||
key?: string // sub variable key
|
||||
comparison_operator?: ComparisonOperator
|
||||
value: string | string[]
|
||||
numberVarType?: NumberVarType
|
||||
sub_variable_condition?: CaseItem
|
||||
}
|
||||
|
||||
export type CaseItem = {
|
||||
logical_operator: LogicalOperator
|
||||
conditions: Condition[]
|
||||
}
|
||||
|
||||
export type HandleAddCondition = (valueSelector: ValueSelector, varItem: Var) => void
|
||||
export type HandleRemoveCondition = (conditionId: string) => void
|
||||
export type HandleUpdateCondition = (conditionId: string, newCondition: Condition) => void
|
||||
export type HandleUpdateConditionLogicalOperator = (value: LogicalOperator) => void
|
||||
|
||||
export type HandleToggleConditionLogicalOperator = () => void
|
||||
|
||||
export type HandleAddSubVariableCondition = (conditionId: string, key?: string) => void
|
||||
export type handleRemoveSubVariableCondition = (conditionId: string, subConditionId: string) => void
|
||||
export type HandleUpdateSubVariableCondition = (conditionId: string, subConditionId: string, newSubCondition: Condition) => void
|
||||
export type HandleToggleSubVariableConditionLogicalOperator = (conditionId: string) => void
|
||||
|
||||
export type LoopNodeType = CommonNodeType & {
|
||||
startNodeType?: BlockEnum
|
||||
start_node_id: string
|
||||
loop_id?: string
|
||||
logical_operator?: LogicalOperator
|
||||
break_conditions?: Condition[]
|
||||
loop_count: number
|
||||
error_handle_mode: ErrorHandleMode // how to handle error in the iteration
|
||||
}
|
||||
329
web/app/components/workflow/nodes/loop/use-config.ts
Normal file
329
web/app/components/workflow/nodes/loop/use-config.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { uuid4 } from '@sentry/utils'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useIsNodeInLoop,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { VarType } from '../../types'
|
||||
import type { ErrorHandleMode, ValueSelector, Var } from '../../types'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils'
|
||||
import useOneStepRun from '../_base/hooks/use-one-step-run'
|
||||
import { getOperators } from './utils'
|
||||
import { LogicalOperator } from './types'
|
||||
import type { HandleAddCondition, HandleAddSubVariableCondition, HandleRemoveCondition, HandleToggleConditionLogicalOperator, HandleToggleSubVariableConditionLogicalOperator, HandleUpdateCondition, HandleUpdateSubVariableCondition, LoopNodeType } from './types'
|
||||
import useIsVarFileAttribute from './use-is-var-file-attribute'
|
||||
|
||||
const DELIMITER = '@@@@@'
|
||||
const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { isNodeInLoop } = useIsNodeInLoop(id)
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload)
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// output
|
||||
const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const beforeNodes = getBeforeNodesInSameBranch(id)
|
||||
const loopChildrenNodes = getLoopNodeChildren(id)
|
||||
const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes]
|
||||
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode)
|
||||
|
||||
// single run
|
||||
const loopInputKey = `${id}.input_selector`
|
||||
const {
|
||||
isShowSingleRun,
|
||||
showSingleRun,
|
||||
hideSingleRun,
|
||||
toVarInputs,
|
||||
runningStatus,
|
||||
handleRun: doHandleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
loopRunResult,
|
||||
} = useOneStepRun<LoopNodeType>({
|
||||
id,
|
||||
data: inputs,
|
||||
loopInputKey,
|
||||
defaultRunInputData: {
|
||||
[loopInputKey]: [''],
|
||||
},
|
||||
})
|
||||
|
||||
const [isShowLoopDetail, {
|
||||
setTrue: doShowLoopDetail,
|
||||
setFalse: doHideLoopDetail,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const hideLoopDetail = useCallback(() => {
|
||||
hideSingleRun()
|
||||
doHideLoopDetail()
|
||||
}, [doHideLoopDetail, hideSingleRun])
|
||||
|
||||
const showLoopDetail = useCallback(() => {
|
||||
doShowLoopDetail()
|
||||
}, [doShowLoopDetail])
|
||||
|
||||
const backToSingleRun = useCallback(() => {
|
||||
hideLoopDetail()
|
||||
showSingleRun()
|
||||
}, [hideLoopDetail, showSingleRun])
|
||||
|
||||
const {
|
||||
getIsVarFileAttribute,
|
||||
} = useIsVarFileAttribute({
|
||||
nodeId: id,
|
||||
})
|
||||
|
||||
const { usedOutVars, allVarObject } = (() => {
|
||||
const vars: ValueSelector[] = []
|
||||
const varObjs: Record<string, boolean> = {}
|
||||
const allVarObject: Record<string, {
|
||||
inSingleRunPassedKey: string
|
||||
}> = {}
|
||||
loopChildrenNodes.forEach((node) => {
|
||||
const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0)
|
||||
nodeVars.forEach((varSelector) => {
|
||||
if (varSelector[0] === id) { // skip Loop node itself variable: item, index
|
||||
return
|
||||
}
|
||||
const isInLoop = isNodeInLoop(varSelector[0])
|
||||
if (isInLoop) // not pass loop inner variable
|
||||
return
|
||||
|
||||
const varSectorStr = varSelector.join('.')
|
||||
if (!varObjs[varSectorStr]) {
|
||||
varObjs[varSectorStr] = true
|
||||
vars.push(varSelector)
|
||||
}
|
||||
let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector)
|
||||
if (typeof passToServerKeys === 'string')
|
||||
passToServerKeys = [passToServerKeys]
|
||||
|
||||
passToServerKeys.forEach((key: string, index: number) => {
|
||||
allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = {
|
||||
inSingleRunPassedKey: key,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
const res = toVarInputs(vars.map((item) => {
|
||||
const varInfo = getNodeInfoById(canChooseVarNodes, item[0])
|
||||
return {
|
||||
label: {
|
||||
nodeType: varInfo?.data.type,
|
||||
nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title
|
||||
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
|
||||
},
|
||||
variable: `${item.join('.')}`,
|
||||
value_selector: item,
|
||||
}
|
||||
}))
|
||||
return {
|
||||
usedOutVars: res,
|
||||
allVarObject,
|
||||
}
|
||||
})()
|
||||
|
||||
const handleRun = useCallback((data: Record<string, any>) => {
|
||||
const formattedData: Record<string, any> = {}
|
||||
Object.keys(allVarObject).forEach((key) => {
|
||||
const [varSectorStr, nodeId] = key.split(DELIMITER)
|
||||
formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr]
|
||||
})
|
||||
formattedData[loopInputKey] = data[loopInputKey]
|
||||
doHandleRun(formattedData)
|
||||
}, [allVarObject, doHandleRun, loopInputKey])
|
||||
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.filter(key => ![loopInputKey].includes(key))
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
const newVars = {
|
||||
...newPayload,
|
||||
[loopInputKey]: runInputData[loopInputKey],
|
||||
}
|
||||
setRunInputData(newVars)
|
||||
}, [loopInputKey, runInputData, setRunInputData])
|
||||
|
||||
const loop = runInputData[loopInputKey]
|
||||
const setLoop = useCallback((newLoop: string[]) => {
|
||||
setRunInputData({
|
||||
...runInputData,
|
||||
[loopInputKey]: newLoop,
|
||||
})
|
||||
}, [loopInputKey, runInputData, setRunInputData])
|
||||
|
||||
const changeErrorResponseMode = useCallback((item: { value: unknown }) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.error_handle_mode = item.value as ErrorHandleMode
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleAddCondition = useCallback<HandleAddCondition>((valueSelector, varItem) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
if (!draft.break_conditions)
|
||||
draft.break_conditions = []
|
||||
|
||||
draft.break_conditions?.push({
|
||||
id: uuid4(),
|
||||
varType: varItem.type,
|
||||
variable_selector: valueSelector,
|
||||
comparison_operator: getOperators(varItem.type, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [getIsVarFileAttribute, inputs, setInputs])
|
||||
|
||||
const handleRemoveCondition = useCallback<HandleRemoveCondition>((conditionId) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.break_conditions = draft.break_conditions?.filter(item => item.id !== conditionId)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleUpdateCondition = useCallback<HandleUpdateCondition>((conditionId, newCondition) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition)
|
||||
Object.assign(targetCondition, newCondition)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>(() => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.logical_operator = draft.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleAddSubVariableCondition = useCallback<HandleAddSubVariableCondition>((conditionId: string, key?: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const condition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
if (!condition?.sub_variable_condition) {
|
||||
condition.sub_variable_condition = {
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
}
|
||||
}
|
||||
const subVarCondition = condition.sub_variable_condition
|
||||
if (subVarCondition) {
|
||||
if (!subVarCondition.conditions)
|
||||
subVarCondition.conditions = []
|
||||
|
||||
const svcComparisonOperators = getOperators(VarType.string, { key: key || '' })
|
||||
|
||||
subVarCondition.conditions.push({
|
||||
id: uuid4(),
|
||||
key: key || '',
|
||||
varType: VarType.string,
|
||||
comparison_operator: (svcComparisonOperators && svcComparisonOperators.length) ? svcComparisonOperators[0] : undefined,
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleRemoveSubVariableCondition = useCallback((conditionId: string, subConditionId: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const condition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
if (!condition?.sub_variable_condition)
|
||||
return
|
||||
const subVarCondition = condition.sub_variable_condition
|
||||
if (subVarCondition)
|
||||
subVarCondition.conditions = subVarCondition.conditions.filter(item => item.id !== subConditionId)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleUpdateSubVariableCondition = useCallback<HandleUpdateSubVariableCondition>((conditionId, subConditionId, newSubCondition) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition && targetCondition.sub_variable_condition) {
|
||||
const targetSubCondition = targetCondition.sub_variable_condition.conditions.find(item => item.id === subConditionId)
|
||||
if (targetSubCondition)
|
||||
Object.assign(targetSubCondition, newSubCondition)
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleToggleSubVariableConditionLogicalOperator = useCallback<HandleToggleSubVariableConditionLogicalOperator>((conditionId) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition && targetCondition.sub_variable_condition)
|
||||
targetCondition.sub_variable_condition.logical_operator = targetCondition.sub_variable_condition.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleUpdateLoopCount = useCallback((value: number) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.loop_count = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterInputVar,
|
||||
childrenNodeVars,
|
||||
loopChildrenNodes,
|
||||
isShowSingleRun,
|
||||
showSingleRun,
|
||||
hideSingleRun,
|
||||
isShowLoopDetail,
|
||||
showLoopDetail,
|
||||
hideLoopDetail,
|
||||
backToSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
usedOutVars,
|
||||
loop,
|
||||
setLoop,
|
||||
loopInputKey,
|
||||
loopRunResult,
|
||||
handleAddCondition,
|
||||
handleRemoveCondition,
|
||||
handleUpdateCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
handleUpdateLoopCount,
|
||||
changeErrorResponseMode,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
146
web/app/components/workflow/nodes/loop/use-interactions.ts
Normal file
146
web/app/components/workflow/nodes/loop/use-interactions.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import { generateNewNode } from '../../utils'
|
||||
import {
|
||||
LOOP_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../loop-start/constants'
|
||||
|
||||
export const useNodeLoopInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
|
||||
const handleNodeLoopRerender = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId)
|
||||
let rightNode: Node
|
||||
let bottomNode: Node
|
||||
|
||||
childrenNodes.forEach((n) => {
|
||||
if (rightNode) {
|
||||
if (n.position.x + n.width! > rightNode.position.x + rightNode.width!)
|
||||
rightNode = n
|
||||
}
|
||||
else {
|
||||
rightNode = n
|
||||
}
|
||||
if (bottomNode) {
|
||||
if (n.position.y + n.height! > bottomNode.position.y + bottomNode.height!)
|
||||
bottomNode = n
|
||||
}
|
||||
else {
|
||||
bottomNode = n
|
||||
}
|
||||
})
|
||||
|
||||
const widthShouldExtend = rightNode! && currentNode.width! < rightNode.position.x + rightNode.width!
|
||||
const heightShouldExtend = bottomNode! && currentNode.height! < bottomNode.position.y + bottomNode.height!
|
||||
|
||||
if (widthShouldExtend || heightShouldExtend) {
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((n) => {
|
||||
if (n.id === nodeId) {
|
||||
if (widthShouldExtend) {
|
||||
n.data.width = rightNode.position.x + rightNode.width! + LOOP_PADDING.right
|
||||
n.width = rightNode.position.x + rightNode.width! + LOOP_PADDING.right
|
||||
}
|
||||
if (heightShouldExtend) {
|
||||
n.data.height = bottomNode.position.y + bottomNode.height! + LOOP_PADDING.bottom
|
||||
n.height = bottomNode.position.y + bottomNode.height! + LOOP_PADDING.bottom
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleNodeLoopChildDrag = useCallback((node: Node) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
const restrictPosition: { x?: number; y?: number } = { x: undefined, y: undefined }
|
||||
|
||||
if (node.data.isInLoop) {
|
||||
const parentNode = nodes.find(n => n.id === node.parentId)
|
||||
|
||||
if (parentNode) {
|
||||
if (node.position.y < LOOP_PADDING.top)
|
||||
restrictPosition.y = LOOP_PADDING.top
|
||||
if (node.position.x < LOOP_PADDING.left)
|
||||
restrictPosition.x = LOOP_PADDING.left
|
||||
if (node.position.x + node.width! > parentNode!.width! - LOOP_PADDING.right)
|
||||
restrictPosition.x = parentNode!.width! - LOOP_PADDING.right - node.width!
|
||||
if (node.position.y + node.height! > parentNode!.height! - LOOP_PADDING.bottom)
|
||||
restrictPosition.y = parentNode!.height! - LOOP_PADDING.bottom - node.height!
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrictPosition,
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleNodeLoopChildSizeChange = useCallback((nodeId: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const parentId = currentNode.parentId
|
||||
|
||||
if (parentId)
|
||||
handleNodeLoopRerender(parentId)
|
||||
}, [store, handleNodeLoopRerender])
|
||||
|
||||
const handleNodeLoopChildrenCopy = useCallback((nodeId: string, newNodeId: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_LOOP_START_NODE)
|
||||
|
||||
return childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${childNodeType}`),
|
||||
loop_id: newNodeId,
|
||||
|
||||
},
|
||||
position: child.position,
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNodeId,
|
||||
extent: child.extent,
|
||||
zIndex: child.zIndex,
|
||||
})
|
||||
newNode.id = `${newNodeId}${newNode.id + index}`
|
||||
return newNode
|
||||
})
|
||||
}, [store, t])
|
||||
|
||||
return {
|
||||
handleNodeLoopRerender,
|
||||
handleNodeLoopChildDrag,
|
||||
handleNodeLoopChildSizeChange,
|
||||
handleNodeLoopChildrenCopy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useIsChatMode, useWorkflow, useWorkflowVariables } from '../../hooks'
|
||||
import type { ValueSelector } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
|
||||
type Params = {
|
||||
nodeId: string
|
||||
}
|
||||
const useIsVarFileAttribute = ({
|
||||
nodeId,
|
||||
}: Params) => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(nodeId)
|
||||
}, [getBeforeNodesInSameBranch, nodeId])
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getIsVarFileAttribute = (variable: ValueSelector) => {
|
||||
if (variable.length !== 3)
|
||||
return false
|
||||
const parentVariable = variable.slice(0, 2)
|
||||
const varType = getCurrentVariableType({
|
||||
valueSelector: parentVariable,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
return varType === VarType.file
|
||||
}
|
||||
return {
|
||||
getIsVarFileAttribute,
|
||||
}
|
||||
}
|
||||
|
||||
export default useIsVarFileAttribute
|
||||
179
web/app/components/workflow/nodes/loop/utils.ts
Normal file
179
web/app/components/workflow/nodes/loop/utils.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { ComparisonOperator } from './types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import type { Branch } from '@/app/components/workflow/types'
|
||||
|
||||
export const isEmptyRelatedOperator = (operator: ComparisonOperator) => {
|
||||
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull, ComparisonOperator.exists, ComparisonOperator.notExists].includes(operator)
|
||||
}
|
||||
|
||||
const notTranslateKey = [
|
||||
ComparisonOperator.equal, ComparisonOperator.notEqual,
|
||||
ComparisonOperator.largerThan, ComparisonOperator.largerThanOrEqual,
|
||||
ComparisonOperator.lessThan, ComparisonOperator.lessThanOrEqual,
|
||||
]
|
||||
|
||||
export const isComparisonOperatorNeedTranslate = (operator?: ComparisonOperator) => {
|
||||
if (!operator)
|
||||
return false
|
||||
return !notTranslateKey.includes(operator)
|
||||
}
|
||||
|
||||
export const getOperators = (type?: VarType, file?: { key: string }) => {
|
||||
const isFile = !!file
|
||||
if (isFile) {
|
||||
const { key } = file
|
||||
|
||||
switch (key) {
|
||||
case 'name':
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case 'type':
|
||||
return [
|
||||
ComparisonOperator.in,
|
||||
ComparisonOperator.notIn,
|
||||
]
|
||||
case 'size':
|
||||
return [
|
||||
ComparisonOperator.largerThan,
|
||||
ComparisonOperator.largerThanOrEqual,
|
||||
ComparisonOperator.lessThan,
|
||||
ComparisonOperator.lessThanOrEqual,
|
||||
]
|
||||
case 'extension':
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
]
|
||||
case 'mime_type':
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case 'transfer_method':
|
||||
return [
|
||||
ComparisonOperator.in,
|
||||
ComparisonOperator.notIn,
|
||||
]
|
||||
case 'url':
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
switch (type) {
|
||||
case VarType.string:
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.number:
|
||||
return [
|
||||
ComparisonOperator.equal,
|
||||
ComparisonOperator.notEqual,
|
||||
ComparisonOperator.largerThan,
|
||||
ComparisonOperator.lessThan,
|
||||
ComparisonOperator.largerThanOrEqual,
|
||||
ComparisonOperator.lessThanOrEqual,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.object:
|
||||
return [
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.file:
|
||||
return [
|
||||
ComparisonOperator.exists,
|
||||
ComparisonOperator.notExists,
|
||||
]
|
||||
case VarType.arrayString:
|
||||
case VarType.arrayNumber:
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.array:
|
||||
case VarType.arrayObject:
|
||||
return [
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.arrayFile:
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.allOf,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
default:
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const comparisonOperatorNotRequireValue = (operator?: ComparisonOperator) => {
|
||||
if (!operator)
|
||||
return false
|
||||
|
||||
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull, ComparisonOperator.exists, ComparisonOperator.notExists].includes(operator)
|
||||
}
|
||||
|
||||
export const branchNameCorrect = (branches: Branch[]) => {
|
||||
const branchLength = branches.length
|
||||
if (branchLength < 2)
|
||||
throw new Error('if-else node branch number must than 2')
|
||||
|
||||
if (branchLength === 2) {
|
||||
return branches.map((branch) => {
|
||||
return {
|
||||
...branch,
|
||||
name: branch.id === 'false' ? 'ELSE' : 'IF',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return branches.map((branch, index) => {
|
||||
return {
|
||||
...branch,
|
||||
name: branch.id === 'false' ? 'ELSE' : `CASE ${index + 1}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user