dify
This commit is contained in:
80
dify/web/app/components/workflow/nodes/loop/add-block.tsx
Normal file
80
dify/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, true)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: loopNodeData.start_node_id,
|
||||
prevNodeSourceHandle: 'source',
|
||||
},
|
||||
)
|
||||
}, [handleNodeAdd, loopNodeData.start_node_id])
|
||||
|
||||
const renderTriggerElement = useCallback((open: boolean) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'system-sm-medium relative inline-flex h-8 cursor-pointer items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 text-components-button-secondary-text shadow-xs backdrop-blur-[5px] hover:bg-components-button-secondary-bg-hover',
|
||||
`${nodesReadOnly && '!cursor-not-allowed bg-components-button-secondary-bg-disabled'}`,
|
||||
open && 'bg-components-button-secondary-bg-hover',
|
||||
)}>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{t('workflow.common.addBlock')}
|
||||
</div>
|
||||
)
|
||||
}, [nodesReadOnly, t])
|
||||
|
||||
return (
|
||||
<div className='absolute left-14 top-7 z-10 flex h-8 items-center'>
|
||||
<div className='group/insert relative h-0.5 w-16 bg-gray-300'>
|
||||
<div className='absolute right-0 top-1/2 h-2 w-0.5 -translate-y-1/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 h-3.5 w-3.5' />
|
||||
{t('workflow.nodes.ifElse.addCondition')}
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
|
||||
<VarReferenceVars
|
||||
vars={variables}
|
||||
isSupportFileVar
|
||||
onChange={handleSelectVariable}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionAdd
|
||||
@@ -0,0 +1,103 @@
|
||||
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 { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import {
|
||||
VariableLabelInNode,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
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 operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
||||
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 h-6 items-center px-1 '>
|
||||
<VariableLabelInNode
|
||||
className='w-0 grow'
|
||||
variables={variableSelector}
|
||||
notShowFullPath
|
||||
/>
|
||||
<div
|
||||
className='mx-1 shrink-0 text-xs font-medium text-text-primary'
|
||||
title={operatorName}
|
||||
>
|
||||
{operatorName}
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-[10px] border-l border-divider-regular pl-[10px]'>
|
||||
{
|
||||
sub_variable_condition?.conditions.map((c: Condition, index) => (
|
||||
<div className='relative flex h-6 items-center space-x-1' key={c.id}>
|
||||
<div className='system-xs-medium text-text-accent'>{c.key}</div>
|
||||
<div className='system-xs-medium text-text-primary'>{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='system-xs-regular text-text-secondary'>{isSelect(c) ? selectName(c) : formatValue(c)}</div>}
|
||||
{index !== sub_variable_condition.conditions.length - 1 && (<div className='absolute bottom-[-10px] right-1 z-10 text-[10px] font-medium uppercase leading-4 text-text-accent'>{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}`)}</div>)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConditionValue)
|
||||
@@ -0,0 +1,57 @@
|
||||
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)
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
|
||||
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),
|
||||
showManageInputField: !!pipelineId,
|
||||
onManageInputField: () => setShowInputFieldPanel?.(true),
|
||||
}}
|
||||
onChange={onChange}
|
||||
editable={!disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionInput
|
||||
@@ -0,0 +1,342 @@
|
||||
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'
|
||||
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
|
||||
|
||||
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 | boolean) => {
|
||||
if (value === condition.value || (isArrayValue && value === (condition.value as string[])?.[0]))
|
||||
return
|
||||
const newCondition = {
|
||||
...condition,
|
||||
value: isArrayValue ? [value as string] : 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]
|
||||
delete draft.key
|
||||
delete draft.sub_variable_condition
|
||||
delete draft.numberVarType
|
||||
})
|
||||
doUpdateCondition(newCondition)
|
||||
setOpen(false)
|
||||
}, [condition, doUpdateCondition])
|
||||
|
||||
return (
|
||||
<div className={cn('mb-1 flex last-of-type:mb-0', className)}>
|
||||
<div className={cn(
|
||||
'grow rounded-lg bg-components-input-bg-normal',
|
||||
isHovered && 'bg-state-destructive-hover',
|
||||
)}>
|
||||
<div className='flex items-center p-1'>
|
||||
<div className='w-0 grow'>
|
||||
{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 cursor-pointer justify-start'>
|
||||
<div className='inline-flex h-6 max-w-full items-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark px-1.5 text-text-accent shadow-xs'>
|
||||
<Variable02 className='h-3.5 w-3.5 shrink-0 text-text-accent' />
|
||||
<div className='system-xs-medium ml-0.5 truncate'>{item?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
: <div className='system-sm-regular text-left text-components-input-text-placeholder'>{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 h-3 w-[1px] 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 && condition.varType !== VarType.boolean && (
|
||||
<div className='max-h-[100px] overflow-y-auto border-t border-t-divider-subtle px-2 py-1'>
|
||||
<ConditionInput
|
||||
disabled={disabled}
|
||||
value={condition.value as string}
|
||||
onChange={handleUpdateConditionValue}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{!comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType === VarType.boolean
|
||||
&& <div className='p-1'>
|
||||
<BoolValue
|
||||
value={condition.value as boolean}
|
||||
onChange={handleUpdateConditionValue}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && (
|
||||
<div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'>
|
||||
<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='rounded-t-none px-2 text-xs'
|
||||
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='ml-1 mt-1 flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={doRemoveCondition}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-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 h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className='flex h-7 cursor-pointer items-center rounded-lg px-3 py-1.5 text-[13px] font-medium text-text-secondary 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] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur 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 bottom-0 left-0 top-0 w-[60px]',
|
||||
isSubVariable && logicalOperator === LogicalOperator.and && 'left-[-10px]',
|
||||
isSubVariable && logicalOperator === LogicalOperator.or && 'left-[-18px]',
|
||||
)}>
|
||||
<div className='absolute bottom-4 left-[46px] top-4 w-2.5 rounded-l-[8px] border border-r-0 border-divider-deep'></div>
|
||||
<div className='absolute right-0 top-1/2 h-[29px] w-4 -translate-y-1/2 bg-components-panel-bg'></div>
|
||||
<div
|
||||
className='absolute right-1 top-1/2 flex h-[21px] -translate-y-1/2 cursor-pointer select-none items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-1 text-[10px] font-semibold text-text-accent-secondary shadow-xs'
|
||||
onClick={() => doToggleConditionLogicalOperator(conditionId)}
|
||||
>
|
||||
{logicalOperator && logicalOperator.toUpperCase()}
|
||||
<RiLoopLeftLine className='ml-0.5 h-3 w-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 cursor-pointer items-center'>
|
||||
<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] h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option}
|
||||
className={cn(
|
||||
'flex h-7 cursor-pointer items-center rounded-md px-3 hover:bg-state-base-hover',
|
||||
'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 h-4 w-[1px] bg-divider-regular'></div>
|
||||
<div className='ml-0.5 w-0 grow'>
|
||||
{
|
||||
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 h-6 items-center p-1 text-[13px] text-components-input-text-placeholder'>
|
||||
<Variable02 className='mr-1 h-4 w-4 shrink-0' />
|
||||
<div className='w-0 grow truncate'>{t('workflow.nodes.ifElse.selectVariable')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className={cn('w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pt-1 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 appearance-none bg-transparent px-2 text-[13px] text-components-input-text-filled outline-none placeholder:text-components-input-text-placeholder', 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='system-sm-regular absolute right-2 top-[50%] translate-y-[-50%] text-text-tertiary'>{unit}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConditionNumberInput)
|
||||
@@ -0,0 +1,87 @@
|
||||
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 { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import {
|
||||
VariableLabelInNode,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
|
||||
type ConditionValueProps = {
|
||||
variableSelector: string[]
|
||||
labelName?: string
|
||||
operator: ComparisonOperator
|
||||
value: string | string[]
|
||||
}
|
||||
const ConditionValue = ({
|
||||
variableSelector,
|
||||
labelName: _labelName,
|
||||
operator,
|
||||
value,
|
||||
}: ConditionValueProps) => {
|
||||
const { t } = useTranslation()
|
||||
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
||||
const notHasValue = comparisonOperatorNotRequireValue(operator)
|
||||
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 h-6 items-center rounded-md bg-workflow-block-parma-bg px-1'>
|
||||
<VariableLabelInNode
|
||||
className='w-0 grow'
|
||||
variables={variableSelector}
|
||||
notShowFullPath
|
||||
/>
|
||||
<div
|
||||
className='mx-1 shrink-0 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,146 @@
|
||||
'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 && 'min-h-[40px] px-3 py-1 ',
|
||||
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 h-3.5 w-3.5' />
|
||||
{t('workflow.nodes.ifElse.addSubVariable')}
|
||||
</Button>
|
||||
)}
|
||||
hideChecked
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ConditionAdd
|
||||
disabled={readOnly}
|
||||
variables={availableVars}
|
||||
onSelectVariable={handleAddCondition!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ConditionWrap)
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Empty = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='system-xs-regular flex h-10 items-center justify-center rounded-[10px] bg-background-section text-text-tertiary'>
|
||||
{t('workflow.nodes.loop.setLoopVariables')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Empty
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import type {
|
||||
LoopVariable,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
import type {
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
ValueType,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
|
||||
|
||||
import {
|
||||
arrayBoolPlaceholder,
|
||||
arrayNumberPlaceholder,
|
||||
arrayObjectPlaceholder,
|
||||
arrayStringPlaceholder,
|
||||
objectPlaceholder,
|
||||
} from '@/app/components/workflow/panel/chat-variable-panel/utils'
|
||||
import ArrayBoolList from '@/app/components/workflow/panel/chat-variable-panel/components/array-bool-list'
|
||||
|
||||
type FormItemProps = {
|
||||
nodeId: string
|
||||
item: LoopVariable
|
||||
onChange: (value: any) => void
|
||||
}
|
||||
const FormItem = ({
|
||||
nodeId,
|
||||
item,
|
||||
onChange,
|
||||
}: FormItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { value_type, var_type, value } = item
|
||||
|
||||
const handleInputChange = useCallback((e: any) => {
|
||||
onChange(e.target.value)
|
||||
}, [onChange])
|
||||
|
||||
const handleChange = useCallback((value: any) => {
|
||||
onChange(value)
|
||||
}, [onChange])
|
||||
|
||||
const filterVar = useCallback((variable: Var) => {
|
||||
return variable.type === var_type
|
||||
}, [var_type])
|
||||
|
||||
const editorMinHeight = useMemo(() => {
|
||||
if (var_type === VarType.arrayObject)
|
||||
return '240px'
|
||||
return '120px'
|
||||
}, [var_type])
|
||||
const placeholder = useMemo(() => {
|
||||
if (var_type === VarType.arrayString)
|
||||
return arrayStringPlaceholder
|
||||
if (var_type === VarType.arrayNumber)
|
||||
return arrayNumberPlaceholder
|
||||
if (var_type === VarType.arrayObject)
|
||||
return arrayObjectPlaceholder
|
||||
if (var_type === VarType.arrayBoolean)
|
||||
return arrayBoolPlaceholder
|
||||
return objectPlaceholder
|
||||
}, [var_type])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
value_type === ValueType.variable && (
|
||||
<VarReferencePicker
|
||||
readonly={false}
|
||||
nodeId={nodeId}
|
||||
isShowNodeName
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
filterVar={filterVar}
|
||||
placeholder={t('workflow.nodes.assigner.setParameter') as string}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.string && (
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
className='min-h-12 w-full'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.number && (
|
||||
<Input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
className='w-full'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.boolean && (
|
||||
<BoolValue
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant
|
||||
&& (var_type === VarType.object || var_type === VarType.arrayString || var_type === VarType.arrayNumber || var_type === VarType.arrayObject)
|
||||
&& (
|
||||
<div className='w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1' style={{ height: editorMinHeight }}>
|
||||
<CodeEditor
|
||||
value={value}
|
||||
isExpand
|
||||
noWrapper
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleChange}
|
||||
className='w-full'
|
||||
placeholder={<div className='whitespace-pre'>{placeholder}</div>}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.arrayBoolean && (
|
||||
<ArrayBoolList
|
||||
className='mt-2'
|
||||
list={value || [false]}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormItem
|
||||
@@ -0,0 +1,28 @@
|
||||
import Empty from './empty'
|
||||
import Item from './item'
|
||||
import type {
|
||||
LoopVariable,
|
||||
LoopVariablesComponentShape,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
|
||||
type LoopVariableProps = {
|
||||
variables?: LoopVariable[]
|
||||
} & LoopVariablesComponentShape
|
||||
|
||||
const LoopVariableComponent = ({
|
||||
variables = [],
|
||||
...restProps
|
||||
}: LoopVariableProps) => {
|
||||
if (!variables.length)
|
||||
return <Empty />
|
||||
|
||||
return variables.map(variable => (
|
||||
<Item
|
||||
key={variable.id}
|
||||
item={variable}
|
||||
{...restProps}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
export default LoopVariableComponent
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
|
||||
type InputModeSelectProps = {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const InputModeSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
}: InputModeSelectProps) => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{
|
||||
label: 'Variable',
|
||||
value: 'variable',
|
||||
},
|
||||
{
|
||||
label: 'Constant',
|
||||
value: 'constant',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PureSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
popupProps={{
|
||||
title: t('workflow.nodes.loop.inputMode'),
|
||||
className: 'w-[132px]',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputModeSelect
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useCallback } from 'react'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InputModeSelect from './input-mode-selec'
|
||||
import VariableTypeSelect from './variable-type-select'
|
||||
import FormItem from './form-item'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type {
|
||||
LoopVariable,
|
||||
LoopVariablesComponentShape,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { ValueType, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type ItemProps = {
|
||||
item: LoopVariable
|
||||
} & LoopVariablesComponentShape
|
||||
const Item = ({
|
||||
nodeId,
|
||||
item,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
}: ItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const checkVariableName = (value: string) => {
|
||||
const { isValid, errorMessageKey } = checkKeys([value], false)
|
||||
if (!isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const handleUpdateItemLabel = useCallback((e: any) => {
|
||||
replaceSpaceWithUnderscoreInVarNameInput(e.target)
|
||||
if (!!e.target.value && !checkVariableName(e.target.value))
|
||||
return
|
||||
handleUpdateLoopVariable(item.id, { label: e.target.value })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const getDefaultValue = useCallback((varType: VarType, valueType: ValueType) => {
|
||||
if(valueType === ValueType.variable)
|
||||
return undefined
|
||||
switch (varType) {
|
||||
case VarType.boolean:
|
||||
return false
|
||||
case VarType.arrayBoolean:
|
||||
return [false]
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleUpdateItemVarType = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { var_type: value, value: getDefaultValue(value, item.value_type) })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemValueType = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { value_type: value, value: getDefaultValue(item.var_type, value) })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemValue = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { value })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
return (
|
||||
<div className='mb-4 flex last-of-type:mb-0'>
|
||||
<div className='w-0 grow'>
|
||||
<div className='mb-1 grid grid-cols-3 gap-1'>
|
||||
<Input
|
||||
value={item.label}
|
||||
onChange={handleUpdateItemLabel}
|
||||
onBlur={e => checkVariableName(e.target.value)}
|
||||
autoFocus={!item.label}
|
||||
placeholder={t('workflow.nodes.loop.variableName')}
|
||||
/>
|
||||
<VariableTypeSelect
|
||||
value={item.var_type}
|
||||
onChange={handleUpdateItemVarType}
|
||||
/>
|
||||
<InputModeSelect
|
||||
value={item.value_type}
|
||||
onChange={handleUpdateItemValueType}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormItem
|
||||
nodeId={nodeId}
|
||||
item={item}
|
||||
onChange={handleUpdateItemValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ActionButton
|
||||
className='shrink-0'
|
||||
size='l'
|
||||
onClick={() => handleRemoveLoopVariable(item.id)}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Item
|
||||
@@ -0,0 +1,59 @@
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type VariableTypeSelectProps = {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const VariableTypeSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
}: VariableTypeSelectProps) => {
|
||||
const options = [
|
||||
{
|
||||
label: 'String',
|
||||
value: VarType.string,
|
||||
},
|
||||
{
|
||||
label: 'Number',
|
||||
value: VarType.number,
|
||||
},
|
||||
{
|
||||
label: 'Object',
|
||||
value: VarType.object,
|
||||
},
|
||||
{
|
||||
label: 'Boolean',
|
||||
value: VarType.boolean,
|
||||
},
|
||||
{
|
||||
label: 'Array[string]',
|
||||
value: VarType.arrayString,
|
||||
},
|
||||
{
|
||||
label: 'Array[number]',
|
||||
value: VarType.arrayNumber,
|
||||
},
|
||||
{
|
||||
label: 'Array[object]',
|
||||
value: VarType.arrayObject,
|
||||
},
|
||||
{
|
||||
label: 'Array[boolean]',
|
||||
value: VarType.arrayBoolean,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PureSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
popupProps={{
|
||||
className: 'w-[132px]',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariableTypeSelect
|
||||
94
dify/web/app/components/workflow/nodes/loop/default.ts
Normal file
94
dify/web/app/components/workflow/nodes/loop/default.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { VarType } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { ComparisonOperator, LogicalOperator, type LoopNodeType } from './types'
|
||||
import { isEmptyRelatedOperator } from './utils'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { LOOP_NODE_MAX_COUNT } from '@/config'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const metaData = genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
sort: 3,
|
||||
type: BlockEnum.Loop,
|
||||
author: 'AICT-Team',
|
||||
isTypeFixed: true,
|
||||
})
|
||||
const nodeDefault: NodeDefault<LoopNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {
|
||||
start_node_id: '',
|
||||
break_conditions: [],
|
||||
loop_count: 10,
|
||||
_children: [],
|
||||
logical_operator: LogicalOperator.and,
|
||||
},
|
||||
checkValid(payload: LoopNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
payload.loop_variables?.forEach((variable) => {
|
||||
if (!variable.label)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
})
|
||||
|
||||
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.varType === VarType.boolean ? condition.value === undefined : !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', 'related_id']
|
||||
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')
|
||||
|
||||
export default nodeDefault
|
||||
61
dify/web/app/components/workflow/nodes/loop/insert-block.tsx
Normal file
61
dify/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, pluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
nextNodeId: startNodeId,
|
||||
nextNodeTargetHandle: 'target',
|
||||
},
|
||||
)
|
||||
}, [startNodeId, handleNodeAdd])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'nopan nodrag',
|
||||
'absolute left-1/2 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 group-hover/insert:block',
|
||||
open && '!block',
|
||||
)}
|
||||
>
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
triggerClassName={() => 'hover:scale-125 transition-all'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(InsertBlock)
|
||||
61
dify/web/app/components/workflow/nodes/loop/node.tsx
Normal file
61
dify/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 h-full min-h-[90px] w-full min-w-[240px] rounded-2xl bg-workflow-canvas-workflow-bg',
|
||||
)}>
|
||||
<Background
|
||||
id={`loop-background-${id}`}
|
||||
className='!z-0 rounded-2xl'
|
||||
gap={[14 / zoom, 14 / zoom]}
|
||||
size={2 / zoom}
|
||||
color='var(--color-workflow-canvas-workflow-dot-color)'
|
||||
/>
|
||||
{
|
||||
data._isCandidate && (
|
||||
<LoopStartNodeDumb />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._children!.length === 1 && (
|
||||
<AddBlock
|
||||
loopNodeId={id}
|
||||
loopNodeData={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Node)
|
||||
118
dify/web/app/components/workflow/nodes/loop/panel.tsx
Normal file
118
dify/web/app/components/workflow/nodes/loop/panel.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import Split from '../_base/components/split'
|
||||
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 LoopVariable from './components/loop-variables'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
|
||||
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,
|
||||
handleAddCondition,
|
||||
handleUpdateCondition,
|
||||
handleRemoveCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
handleUpdateLoopCount,
|
||||
handleAddLoopVariable,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div>
|
||||
<Field
|
||||
title={<div className='pl-3'>{t('workflow.nodes.loop.loopVariables')}</div>}
|
||||
operations={
|
||||
<div
|
||||
className='mr-4 flex h-5 w-5 cursor-pointer items-center justify-center'
|
||||
onClick={handleAddLoopVariable}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='px-4'>
|
||||
<LoopVariable
|
||||
variables={inputs.loop_variables}
|
||||
nodeId={id}
|
||||
handleRemoveLoopVariable={handleRemoveLoopVariable}
|
||||
handleUpdateLoopVariable={handleUpdateLoopVariable}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<Split className='my-2' />
|
||||
<Field
|
||||
title={<div className='pl-3'>{t(`${i18nPrefix}.breakCondition`)}</div>}
|
||||
tooltip={t(`${i18nPrefix}.breakConditionTip`)}
|
||||
>
|
||||
<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 className='mt-2' />
|
||||
<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> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
94
dify/web/app/components/workflow/nodes/loop/types.ts
Normal file
94
dify/web/app/components/workflow/nodes/loop/types.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { VarType as NumberVarType } from '../tool/types'
|
||||
import type {
|
||||
BlockEnum,
|
||||
CommonNodeType,
|
||||
ErrorHandleMode,
|
||||
ValueSelector,
|
||||
ValueType,
|
||||
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[] | boolean
|
||||
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 LoopVariable = {
|
||||
id: string
|
||||
label: string
|
||||
var_type: VarType
|
||||
value_type: ValueType
|
||||
value: any
|
||||
}
|
||||
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
|
||||
loop_variables?: LoopVariable[]
|
||||
}
|
||||
|
||||
export type HandleUpdateLoopVariable = (id: string, updateData: Partial<LoopVariable>) => void
|
||||
export type HandleRemoveLoopVariable = (id: string) => void
|
||||
|
||||
export type LoopVariablesComponentShape = {
|
||||
nodeId: string
|
||||
handleRemoveLoopVariable: HandleRemoveLoopVariable
|
||||
handleUpdateLoopVariable: HandleUpdateLoopVariable
|
||||
}
|
||||
253
dify/web/app/components/workflow/nodes/loop/use-config.ts
Normal file
253
dify/web/app/components/workflow/nodes/loop/use-config.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { ValueType, VarType } from '../../types'
|
||||
import type { ErrorHandleMode, Var } from '../../types'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { toNodeOutputVars } from '../_base/components/variable/utils'
|
||||
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'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const isChatMode = useIsChatMode()
|
||||
const conversationVariables = useStore(s => s.conversationVariables)
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload)
|
||||
const inputsRef = useRef(inputs)
|
||||
const handleInputsChange = useCallback((newInputs: LoopNodeType) => {
|
||||
inputsRef.current = newInputs
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// output
|
||||
const { getLoopNodeChildren } = useWorkflow()
|
||||
const loopChildrenNodes = [{ id, data: payload } as any, ...getLoopNodeChildren(id)]
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const allPluginInfoList = {
|
||||
buildInTools: buildInTools || [],
|
||||
customTools: customTools || [],
|
||||
workflowTools: workflowTools || [],
|
||||
mcpTools: mcpTools || [],
|
||||
dataSourceList: dataSourceList || [],
|
||||
}
|
||||
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables, [], allPluginInfoList)
|
||||
|
||||
const {
|
||||
getIsVarFileAttribute,
|
||||
} = useIsVarFileAttribute({
|
||||
nodeId: id,
|
||||
})
|
||||
|
||||
const changeErrorResponseMode = useCallback((item: { value: unknown }) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.error_handle_mode = item.value as ErrorHandleMode
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [inputs, handleInputsChange])
|
||||
|
||||
const handleAddCondition = useCallback<HandleAddCondition>((valueSelector, varItem) => {
|
||||
const newInputs = produce(inputsRef.current, (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: varItem.type === VarType.boolean ? 'false' : '',
|
||||
})
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [getIsVarFileAttribute, handleInputsChange])
|
||||
|
||||
const handleRemoveCondition = useCallback<HandleRemoveCondition>((conditionId) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.break_conditions = draft.break_conditions?.filter(item => item.id !== conditionId)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateCondition = useCallback<HandleUpdateCondition>((conditionId, newCondition) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition)
|
||||
Object.assign(targetCondition, newCondition)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>(() => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.logical_operator = draft.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleAddSubVariableCondition = useCallback<HandleAddSubVariableCondition>((conditionId: string, key?: string) => {
|
||||
const newInputs = produce(inputsRef.current, (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: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleRemoveSubVariableCondition = useCallback((conditionId: string, subConditionId: string) => {
|
||||
const newInputs = produce(inputsRef.current, (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)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateSubVariableCondition = useCallback<HandleUpdateSubVariableCondition>((conditionId, subConditionId, newSubCondition) => {
|
||||
const newInputs = produce(inputsRef.current, (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)
|
||||
}
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleToggleSubVariableConditionLogicalOperator = useCallback<HandleToggleSubVariableConditionLogicalOperator>((conditionId) => {
|
||||
const newInputs = produce(inputsRef.current, (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
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateLoopCount = useCallback((value: number) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.loop_count = value
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleAddLoopVariable = useCallback(() => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (!draft.loop_variables)
|
||||
draft.loop_variables = []
|
||||
|
||||
draft.loop_variables.push({
|
||||
id: uuid4(),
|
||||
label: '',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.constant,
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleRemoveLoopVariable = useCallback((id: string) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.loop_variables = draft.loop_variables?.filter(item => item.id !== id)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateLoopVariable = useCallback((id: string, updateData: any) => {
|
||||
const loopVariables = inputsRef.current.loop_variables || []
|
||||
const index = loopVariables.findIndex(item => item.id === id)
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (index > -1) {
|
||||
draft.loop_variables![index] = {
|
||||
...draft.loop_variables![index],
|
||||
...updateData,
|
||||
}
|
||||
}
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterInputVar,
|
||||
childrenNodeVars,
|
||||
loopChildrenNodes,
|
||||
handleAddCondition,
|
||||
handleRemoveCondition,
|
||||
handleUpdateCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
handleUpdateLoopCount,
|
||||
changeErrorResponseMode,
|
||||
handleAddLoopVariable,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
151
dify/web/app/components/workflow/nodes/loop/use-interactions.ts
Normal file
151
dify/web/app/components/workflow/nodes/loop/use-interactions.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../../utils'
|
||||
import {
|
||||
LOOP_PADDING,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../loop-start/constants'
|
||||
import { useNodesMetaData } from '@/app/components/workflow/hooks'
|
||||
|
||||
export const useNodeLoopInteractions = () => {
|
||||
const store = useStoreApi()
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
|
||||
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 {
|
||||
defaultValue,
|
||||
} = nodesMetaDataMap![childNodeType]
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...defaultValue,
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${defaultValue.title} ${nodesWithSameType.length + 1}` : defaultValue.title,
|
||||
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, nodesMetaDataMap])
|
||||
|
||||
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
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import formatTracing from '@/app/components/workflow/run/utils/format-log'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useIsNodeInLoop, useWorkflow } from '../../hooks'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar } from '../_base/components/variable/utils'
|
||||
import type { InputVar, ValueSelector, Variable } from '../../types'
|
||||
import type { CaseItem, Condition, LoopNodeType } from './types'
|
||||
import { ValueType } from '@/app/components/workflow/types'
|
||||
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
payload: LoopNodeType
|
||||
runInputData: Record<string, any>
|
||||
runResult: NodeTracing
|
||||
loopRunResult: NodeTracing[]
|
||||
setRunInputData: (data: Record<string, any>) => void
|
||||
toVarInputs: (variables: Variable[]) => InputVar[]
|
||||
varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[]
|
||||
}
|
||||
|
||||
const useSingleRunFormParams = ({
|
||||
id,
|
||||
payload,
|
||||
runInputData,
|
||||
runResult,
|
||||
loopRunResult,
|
||||
setRunInputData,
|
||||
toVarInputs,
|
||||
varSelectorsToVarInputs,
|
||||
}: Params) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { isNodeInLoop } = useIsNodeInLoop(id)
|
||||
|
||||
const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const loopChildrenNodes = getLoopNodeChildren(id)
|
||||
const beforeNodes = getBeforeNodesInSameBranch(id)
|
||||
const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes]
|
||||
|
||||
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 nodeInfo = useMemo(() => {
|
||||
const formattedNodeInfo = formatTracing(loopRunResult, t)[0]
|
||||
|
||||
if (runResult && formattedNodeInfo) {
|
||||
return {
|
||||
...formattedNodeInfo,
|
||||
execution_metadata: {
|
||||
...runResult.execution_metadata,
|
||||
...formattedNodeInfo.execution_metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return formattedNodeInfo
|
||||
}, [runResult, loopRunResult, t])
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
setRunInputData(newPayload)
|
||||
}, [setRunInputData])
|
||||
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const getVarSelectorsFromCase = (caseItem: CaseItem): ValueSelector[] => {
|
||||
const vars: ValueSelector[] = []
|
||||
if (caseItem.conditions && caseItem.conditions.length) {
|
||||
caseItem.conditions.forEach((condition) => {
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const conditionVars = getVarSelectorsFromCondition(condition)
|
||||
vars.push(...conditionVars)
|
||||
})
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
const getVarSelectorsFromCondition = (condition: Condition) => {
|
||||
const vars: ValueSelector[] = []
|
||||
if (condition.variable_selector)
|
||||
vars.push(condition.variable_selector)
|
||||
|
||||
if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length)
|
||||
vars.push(...getVarSelectorsFromCase(condition.sub_variable_condition))
|
||||
return vars
|
||||
}
|
||||
|
||||
const forms = (() => {
|
||||
const allInputs: ValueSelector[] = []
|
||||
payload.break_conditions?.forEach((condition) => {
|
||||
const vars = getVarSelectorsFromCondition(condition)
|
||||
allInputs.push(...vars)
|
||||
})
|
||||
|
||||
payload.loop_variables?.forEach((loopVariable) => {
|
||||
if (loopVariable.value_type === ValueType.variable)
|
||||
allInputs.push(loopVariable.value)
|
||||
})
|
||||
const inputVarsFromValue: InputVar[] = []
|
||||
const varInputs = [...varSelectorsToVarInputs(allInputs), ...inputVarsFromValue]
|
||||
const existVarsKey: Record<string, boolean> = {}
|
||||
const uniqueVarInputs: InputVar[] = []
|
||||
varInputs.forEach((input) => {
|
||||
if (!input)
|
||||
return
|
||||
if (!existVarsKey[input.variable]) {
|
||||
existVarsKey[input.variable] = true
|
||||
uniqueVarInputs.push(input)
|
||||
}
|
||||
})
|
||||
return [
|
||||
{
|
||||
inputs: [...usedOutVars, ...uniqueVarInputs],
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
]
|
||||
})()
|
||||
|
||||
const getVarFromCaseItem = (caseItem: CaseItem): ValueSelector[] => {
|
||||
const vars: ValueSelector[] = []
|
||||
if (caseItem.conditions && caseItem.conditions.length) {
|
||||
caseItem.conditions.forEach((condition) => {
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const conditionVars = getVarFromCondition(condition)
|
||||
vars.push(...conditionVars)
|
||||
})
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
const getVarFromCondition = (condition: Condition): ValueSelector[] => {
|
||||
const vars: ValueSelector[] = []
|
||||
if (condition.variable_selector)
|
||||
vars.push(condition.variable_selector)
|
||||
|
||||
if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length)
|
||||
vars.push(...getVarFromCaseItem(condition.sub_variable_condition))
|
||||
return vars
|
||||
}
|
||||
|
||||
const getDependentVars = () => {
|
||||
const vars: ValueSelector[] = usedOutVars.map(item => item.variable.split('.'))
|
||||
payload.break_conditions?.forEach((condition) => {
|
||||
const conditionVars = getVarFromCondition(condition)
|
||||
vars.push(...conditionVars)
|
||||
})
|
||||
payload.loop_variables?.forEach((loopVariable) => {
|
||||
if (loopVariable.value_type === ValueType.variable)
|
||||
vars.push(loopVariable.value)
|
||||
})
|
||||
const hasFilterLoopVars = vars.filter(item => item[0] !== id)
|
||||
return hasFilterLoopVars
|
||||
}
|
||||
|
||||
return {
|
||||
forms,
|
||||
nodeInfo,
|
||||
allVarObject,
|
||||
getDependentVars,
|
||||
}
|
||||
}
|
||||
|
||||
export default useSingleRunFormParams
|
||||
186
dify/web/app/components/workflow/nodes/loop/utils.ts
Normal file
186
dify/web/app/components/workflow/nodes/loop/utils.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
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.boolean:
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
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