Feat/attachments (#9526)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
zxhlyh
2024-10-21 10:32:37 +08:00
committed by GitHub
parent 4fd2743efa
commit 7a1d6fe509
445 changed files with 11759 additions and 6922 deletions

View File

@@ -64,6 +64,7 @@ const ConditionAdd = ({
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
<VarReferenceVars
vars={variables}
isSupportFileVar
onChange={handleSelectVariable}
/>
</div>

View File

@@ -0,0 +1,115 @@
import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import { ComparisonOperator, type Condition } from '../types'
import {
comparisonOperatorNotRequireValue,
isComparisonOperatorNeedTranslate,
isEmptyRelatedOperator,
} from '../utils'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default'
import type { ValueSelector } from '../../../types'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import cn from '@/utils/classnames'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
const i18nPrefix = 'workflow.nodes.ifElse'
type ConditionValueProps = {
condition: Condition
}
const ConditionValue = ({
condition,
}: ConditionValueProps) => {
const { t } = useTranslation()
const {
variable_selector,
comparison_operator: operator,
sub_variable_condition,
} = condition
const variableSelector = variable_selector as ValueSelector
const variableName = (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
const notHasValue = comparisonOperatorNotRequireValue(operator)
const isEnvVar = isENV(variableSelector)
const isChatVar = isConversationVar(variableSelector)
const formatValue = useCallback((c: Condition) => {
const notHasValue = comparisonOperatorNotRequireValue(c.comparison_operator)
if (notHasValue)
return ''
const value = c.value as string
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
const arr: string[] = b.split('.')
if (isSystemVar(arr))
return `{{${b}}}`
return `{{${arr.slice(1).join('.')}}}`
})
}, [])
const isSelect = useCallback((c: Condition) => {
return c.comparison_operator === ComparisonOperator.in || c.comparison_operator === ComparisonOperator.notIn
}, [])
const selectName = useCallback((c: Condition) => {
const isSelect = c.comparison_operator === ComparisonOperator.in || c.comparison_operator === ComparisonOperator.notIn
if (isSelect) {
const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(c.value) ? c.value[0] : c.value))[0]
return name
? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/{{#([^#]*)#}}/g, (a, b) => {
const arr: string[] = b.split('.')
if (isSystemVar(arr))
return `{{${b}}}`
return `{{${arr.slice(1).join('.')}}}`
})
: ''
}
return ''
}, [])
return (
<div className='rounded-md bg-workflow-block-parma-bg'>
<div className='flex items-center px-1 h-6 '>
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
{isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div
className={cn(
'shrink-0 truncate text-xs font-medium text-text-accent',
!notHasValue && 'max-w-[70px]',
)}
title={variableName}
>
{variableName}
</div>
<div
className='shrink-0 mx-1 text-xs font-medium text-text-primary'
title={operatorName}
>
{operatorName}
</div>
</div>
<div className='ml-[10px] pl-[10px] border-l border-divider-regular'>
{
sub_variable_condition?.conditions.map((c: Condition, index) => (
<div className='relative flex items-center h-6 space-x-1' key={c.id}>
<div className='text-text-accent system-xs-medium'>{c.key}</div>
<div className='text-text-primary system-xs-medium'>{isComparisonOperatorNeedTranslate(c.comparison_operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${c.comparison_operator}`) : c.comparison_operator}</div>
{c.comparison_operator && !isEmptyRelatedOperator(c.comparison_operator) && <div className='text-text-secondary system-xs-regular'>{isSelect(c) ? selectName(c) : formatValue(c)}</div>}
{index !== sub_variable_condition.conditions.length - 1 && (<div className='absolute z-10 right-1 bottom-[-10px] leading-4 text-[10px] font-medium text-text-accent uppercase'>{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}`)}</div>)}
</div>
))
}
</div>
</div>
)
}
export default memo(ConditionValue)

View File

@@ -1,64 +1,111 @@
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 {
ComparisonOperator,
Condition,
HandleAddSubVariableCondition,
HandleRemoveCondition,
HandleToggleSubVariableConditionLogicalOperator,
HandleUpdateCondition,
HandleUpdateSubVariableCondition,
handleRemoveSubVariableCondition,
} from '../../types'
import { comparisonOperatorNotRequireValue } from '../../utils'
import {
ComparisonOperator,
} from '../../types'
import { comparisonOperatorNotRequireValue, getOperators } from '../../utils'
import ConditionNumberInput from '../condition-number-input'
import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../default'
import ConditionWrap from '../condition-wrap'
import ConditionOperator from './condition-operator'
import ConditionInput from './condition-input'
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
import type {
Node,
NodeOutPutVar,
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'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
type ConditionItemProps = {
className?: string
disabled?: boolean
caseId: string
condition: Condition
onRemoveCondition: HandleRemoveCondition
onUpdateCondition: HandleUpdateCondition
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
nodesOutputVars: NodeOutPutVar[]
availableNodes: Node[]
numberVariables: NodeOutPutVar[]
filterVar: (varPayload: Var) => boolean
}
const ConditionItem = ({
className,
disabled,
caseId,
conditionId,
condition,
file,
isSubVariableKey,
isValueFieldShort,
onRemoveCondition,
onUpdateCondition,
onAddSubVariableCondition,
onRemoveSubVariableCondition,
onUpdateSubVariableCondition,
onToggleSubVariableConditionLogicalOperator,
nodeId,
nodesOutputVars,
availableNodes,
numberVariables,
filterVar,
}: ConditionItemProps) => {
const { t } = useTranslation()
const [isHovered, setIsHovered] = useState(false)
const doUpdateCondition = useCallback((newCondition: Condition) => {
if (isSubVariableKey)
onUpdateSubVariableCondition?.(caseId, conditionId, condition.id, newCondition)
else
onUpdateCondition?.(caseId, condition.id, newCondition)
}, [caseId, 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,
}
onUpdateCondition(caseId, condition.id, newCondition)
}, [caseId, condition, onUpdateCondition])
const handleUpdateConditionValue = useCallback((value: string) => {
const newCondition = {
...condition,
value,
}
onUpdateCondition(caseId, condition.id, newCondition)
}, [caseId, condition, onUpdateCondition])
doUpdateCondition(newCondition)
}, [condition, doUpdateCondition])
const handleUpdateConditionNumberVarType = useCallback((numberVarType: NumberVarType) => {
const newCondition = {
@@ -66,37 +113,138 @@ const ConditionItem = ({
numberVarType,
value: '',
}
onUpdateCondition(caseId, condition.id, newCondition)
}, [caseId, condition, onUpdateCondition])
doUpdateCondition(newCondition)
}, [condition, doUpdateCondition])
const isSubVariable = condition.varType === VarType.arrayFile && [ComparisonOperator.contains, ComparisonOperator.notContains, ComparisonOperator.allOf].includes(condition.comparison_operator!)
const fileAttr = useMemo(() => {
if (file)
return file
if (isSubVariableKey) {
return {
key: condition.key!,
}
}
return undefined
}, [condition.key, file, isSubVariableKey])
const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type'
const handleUpdateConditionValue = useCallback((value: string) => {
if (value === condition.value || (isArrayValue && value === condition.value?.[0]))
return
const newCondition = {
...condition,
value: isArrayValue ? [value] : value,
}
doUpdateCondition(newCondition)
}, [condition, doUpdateCondition, fileAttr])
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?.(caseId, conditionId, condition.id, newCondition)
}, [caseId, condition, conditionId, onUpdateSubVariableCondition])
const doRemoveCondition = useCallback(() => {
if (isSubVariableKey)
onRemoveSubVariableCondition?.(caseId, conditionId, condition.id)
else
onRemoveCondition?.(caseId, condition.id)
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
return (
<div className='flex mb-1 last-of-type:mb-0'>
<div className={cn('flex mb-1 last-of-type:mb-0', className)}>
<div className={cn(
'grow bg-components-input-bg-normal rounded-lg',
isHovered && 'bg-state-destructive-hover',
)}>
<div className='flex items-center p-1'>
<div className='grow w-0'>
<VariableTag
valueSelector={condition.variable_selector}
varType={condition.varType}
availableNodes={availableNodes}
/>
{isSubVarSelect
? (
<Select
wrapperClassName='h-6'
className='pl-0 text-xs'
optionWrapClassName='w-[165px] max-h-none'
defaultValue={condition.key}
items={subVarOptions}
onSelect={item => handleSubVarKeyChange(item.value as string)}
renderTrigger={item => (
item
? <div className='flex justify-start cursor-pointer'>
<div className='inline-flex max-w-full px-1.5 items-center h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark shadow-xs text-text-accent'>
<Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />
<div className='ml-0.5 truncate system-xs-medium'>{item?.name}</div>
</div>
</div>
: <div className='text-left text-components-input-text-placeholder system-sm-regular'>{t('common.placeholder.select')}</div>
)}
hideChecked
/>
)
: (
<VariableTag
valueSelector={condition.variable_selector || []}
varType={condition.varType}
availableNodes={availableNodes}
isShort
/>
)}
</div>
<div className='mx-1 w-[1px] h-3 bg-divider-regular'></div>
<ConditionOperator
disabled={disabled}
disabled={!canChooseOperator}
varType={condition.varType}
value={condition.comparison_operator}
onSelect={handleUpdateConditionOperator}
file={fileAttr}
/>
</div>
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType !== VarType.number && (
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && (
<div className='px-2 py-1 max-h-[100px] border-t border-t-divider-subtle overflow-y-auto'>
<ConditionInput
disabled={disabled}
value={condition.value}
value={condition.value as string}
onChange={handleUpdateConditionValue}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
@@ -105,14 +253,52 @@ const ConditionItem = ({
)
}
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType === VarType.number && (
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && (
<div className='px-2 py-1 pt-[3px] border-t border-t-divider-subtle'>
<ConditionNumberInput
numberVarType={condition.numberVarType}
onNumberVarTypeChange={handleUpdateConditionNumberVarType}
value={condition.value}
value={condition.value as string}
onValueChange={handleUpdateConditionValue}
variables={numberVariables}
isShort={isValueFieldShort}
unit={fileAttr?.key === 'size' ? 'Byte' : undefined}
/>
</div>
)
}
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && isSelect && (
<div className='border-t border-t-divider-subtle'>
<Select
wrapperClassName='h-8'
className='px-2 text-xs rounded-t-none'
defaultValue={isArrayValue ? (condition.value as string[])?.[0] : (condition.value as string)}
items={selectOptions}
onSelect={item => handleUpdateConditionValue(item.value as string)}
hideChecked
notClearable
/>
</div>
)
}
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && isSubVariable && (
<div className='p-1'>
<ConditionWrap
isSubVariable
caseId={caseId}
conditionId={conditionId}
readOnly={!!disabled}
cases={condition.sub_variable_condition ? [condition.sub_variable_condition] : []}
handleAddSubVariableCondition={onAddSubVariableCondition}
handleRemoveSubVariableCondition={onRemoveSubVariableCondition}
handleUpdateSubVariableCondition={onUpdateSubVariableCondition}
handleToggleSubVariableConditionLogicalOperator={onToggleSubVariableConditionLogicalOperator}
nodeId={nodeId}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
filterVar={filterVar}
/>
</div>
)
@@ -122,7 +308,7 @@ const ConditionItem = ({
className='shrink-0 flex items-center justify-center ml-1 mt-1 w-6 h-6 rounded-lg cursor-pointer hover:bg-state-destructive-hover text-text-tertiary hover:text-text-destructive'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={() => onRemoveCondition(caseId, condition.id)}
onClick={doRemoveCondition}
>
<RiDeleteBinLine className='w-4 h-4' />
</div>

View File

@@ -17,14 +17,18 @@ 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) => {
@@ -32,15 +36,14 @@ const ConditionOperator = ({
const [open, setOpen] = useState(false)
const options = useMemo(() => {
return getOperators(varType).map((o) => {
return getOperators(varType, file).map((o) => {
return {
label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o,
value: o,
}
})
}, [t, varType])
const selectedOption = options.find(o => o.value === value)
}, [t, varType, file])
const selectedOption = options.find(o => Array.isArray(value) ? o.value === value[0] : o.value === value)
return (
<PortalToFollowElem
open={open}
@@ -53,7 +56,7 @@ const ConditionOperator = ({
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<Button
className={cn('shrink-0', !selectedOption && 'opacity-50')}
className={cn('shrink-0', !selectedOption && 'opacity-50', className)}
size='small'
variant='ghost'
disabled={disabled}
@@ -61,7 +64,7 @@ const ConditionOperator = ({
{
selectedOption
? selectedOption.label
: 'select'
: t(`${i18nPrefix}.select`)
}
<RiArrowDownSLine className='ml-1 w-3.5 h-3.5' />
</Button>

View File

@@ -1,51 +1,101 @@
import { RiLoopLeftLine } from '@remixicon/react'
import { LogicalOperator } from '../../types'
import type {
CaseItem,
HandleRemoveCondition,
HandleUpdateCondition,
HandleUpdateConditionLogicalOperator,
import { useCallback, useMemo } from 'react'
import {
type CaseItem,
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,
Var,
} from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type ConditionListProps = {
isSubVariable?: boolean
disabled?: boolean
caseId: string
conditionId?: string
caseItem: CaseItem
onUpdateCondition: HandleUpdateCondition
onUpdateConditionLogicalOperator: HandleUpdateConditionLogicalOperator
onRemoveCondition: HandleRemoveCondition
onRemoveCondition?: HandleRemoveCondition
onUpdateCondition?: HandleUpdateCondition
onToggleConditionLogicalOperator?: HandleToggleConditionLogicalOperator
nodeId: string
nodesOutputVars: NodeOutPutVar[]
availableNodes: Node[]
numberVariables: NodeOutPutVar[]
filterVar: (varPayload: Var) => boolean
varsIsVarFileAttribute: Record<string, boolean>
onAddSubVariableCondition?: HandleAddSubVariableCondition
onRemoveSubVariableCondition?: handleRemoveSubVariableCondition
onUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
onToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
}
const ConditionList = ({
isSubVariable,
disabled,
caseId,
conditionId,
caseItem,
onUpdateCondition,
onUpdateConditionLogicalOperator,
onRemoveCondition,
onToggleConditionLogicalOperator,
onAddSubVariableCondition,
onRemoveSubVariableCondition,
onUpdateSubVariableCondition,
onToggleSubVariableConditionLogicalOperator,
nodeId,
nodesOutputVars,
availableNodes,
numberVariables,
varsIsVarFileAttribute,
filterVar,
}: ConditionListProps) => {
const { conditions, logical_operator } = caseItem
const doToggleConditionLogicalOperator = useCallback(() => {
if (isSubVariable)
onToggleSubVariableConditionLogicalOperator?.(caseId!, conditionId!)
else
onToggleConditionLogicalOperator?.(caseId)
}, [caseId, conditionId, 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 logical_operator === LogicalOperator.and ? 'pl-[51px]' : 'pl-[42px]'
}, [conditions.length, isSubVariable, logical_operator])
return (
<div className='relative pl-[60px]'>
<div className={cn('relative', !isSubVariable && 'pl-[60px]')}>
{
conditions.length > 1 && (
<div className='absolute top-0 bottom-0 left-0 w-[60px]'>
<div className={cn(
'absolute top-0 bottom-0 left-0 w-[60px]',
isSubVariable && logical_operator === LogicalOperator.and && 'left-[-10px]',
isSubVariable && logical_operator === LogicalOperator.or && 'left-[-18px]',
)}>
<div className='absolute top-4 bottom-4 left-[46px] w-2.5 border border-divider-deep rounded-l-[8px] border-r-0'></div>
<div className='absolute top-1/2 -translate-y-1/2 right-0 w-4 h-[29px] bg-components-panel-bg'></div>
<div
className='absolute top-1/2 right-1 -translate-y-1/2 flex items-center px-1 h-[21px] rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs bg-components-button-secondary-bg text-text-accent-secondary text-[10px] font-semibold cursor-pointer'
onClick={() => {
onUpdateConditionLogicalOperator(caseItem.case_id, caseItem.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and)
}}
className='absolute top-1/2 right-1 -translate-y-1/2 flex items-center px-1 h-[21px] rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs bg-components-button-secondary-bg text-text-accent-secondary text-[10px] font-semibold cursor-pointer select-none'
onClick={doToggleConditionLogicalOperator}
>
{logical_operator.toUpperCase()}
<RiLoopLeftLine className='ml-0.5 w-3 h-3' />
@@ -57,14 +107,25 @@ const ConditionList = ({
caseItem.conditions.map(condition => (
<ConditionItem
key={condition.id}
className={conditionItemClassName}
disabled={disabled}
caseId={caseItem.case_id}
caseId={caseId}
conditionId={isSubVariable ? conditionId! : condition.id}
condition={condition}
isValueFieldShort={isValueFieldShort}
onUpdateCondition={onUpdateCondition}
onRemoveCondition={onRemoveCondition}
onAddSubVariableCondition={onAddSubVariableCondition}
onRemoveSubVariableCondition={onRemoveSubVariableCondition}
onUpdateSubVariableCondition={onUpdateSubVariableCondition}
onToggleSubVariableConditionLogicalOperator={onToggleSubVariableConditionLogicalOperator}
nodeId={nodeId}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
filterVar={filterVar}
numberVariables={numberVariables}
file={varsIsVarFileAttribute[condition.id] ? { key: (condition.variable_selector || []).slice(-1)[0] } : undefined}
isSubVariableKey={isSubVariable}
/>
))
}

View File

@@ -6,6 +6,7 @@ import {
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 {
@@ -35,6 +36,8 @@ type ConditionNumberInputProps = {
value: string
onValueChange: (v: string) => void
variables: NodeOutPutVar[]
isShort?: boolean
unit?: string
}
const ConditionNumberInput = ({
numberVarType = NumberVarType.constant,
@@ -42,10 +45,16 @@ const ConditionNumberInput = ({
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)
@@ -111,20 +120,21 @@ const ConditionNumberInput = ({
<VariableTag
valueSelector={variableTransformer(value) as string[]}
varType={VarType.number}
isShort={isShort}
/>
)
}
{
!value && (
<div className='flex items-center p-1 h-6 text-components-input-text-placeholder text-[13px]'>
<Variable02 className='mr-1 w-4 h-4' />
{t('workflow.nodes.ifElse.selectVariable')}
<Variable02 className='shrink-0 mr-1 w-4 h-4' />
<div className='w-0 grow truncate'>{t('workflow.nodes.ifElse.selectVariable')}</div>
</div>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
<div className={cn('w-[296px] pt-1 bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg', isShort && 'w-[200px]')}>
<VarReferenceVars
vars={variables}
onChange={handleSelectVariable}
@@ -136,13 +146,18 @@ const ConditionNumberInput = ({
}
{
numberVarType === NumberVarType.constant && (
<input
className='block w-full px-2 text-[13px] text-components-input-text-filled placeholder:text-components-input-text-placeholder outline-none appearance-none bg-transparent'
type='number'
value={value}
onChange={e => onValueChange(e.target.value)}
placeholder={t('workflow.nodes.ifElse.enterValue') || ''}
/>
<div className=' relative'>
<input
className={cn('block w-full px-2 text-[13px] text-components-input-text-filled placeholder:text-components-input-text-placeholder outline-none appearance-none bg-transparent', unit && 'pr-6')}
type='number'
value={value}
onChange={e => onValueChange(e.target.value)}
placeholder={t('workflow.nodes.ifElse.enterValue') || ''}
onFocus={setFocus}
onBlur={setBlur}
/>
{!isFocus && unit && <div className='absolute right-2 top-[50%] translate-y-[-50%] text-text-tertiary system-sm-regular'>{unit}</div>}
</div>
)
}
</div>

View File

@@ -3,11 +3,12 @@ import {
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import type { ComparisonOperator } from '../types'
import { ComparisonOperator } from '../types'
import {
comparisonOperatorNotRequireValue,
isComparisonOperatorNeedTranslate,
} from '../utils'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import cn from '@/utils/classnames'
@@ -15,16 +16,18 @@ import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow
type ConditionValueProps = {
variableSelector: string[]
labelName?: string
operator: ComparisonOperator
value: string
value: string | string[]
}
const ConditionValue = ({
variableSelector,
labelName,
operator,
value,
}: ConditionValueProps) => {
const { t } = useTranslation()
const variableName = isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.')
const variableName = labelName || (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
const notHasValue = comparisonOperatorNotRequireValue(operator)
const isEnvVar = isENV(variableSelector)
@@ -33,6 +36,9 @@ const ConditionValue = ({
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))
@@ -42,6 +48,23 @@ const ConditionValue = ({
})
}, [notHasValue, value])
const isSelect = operator === ComparisonOperator.in || operator === ComparisonOperator.notIn
const selectName = useMemo(() => {
if (isSelect) {
const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(value) ? value[0] : value))[0]
return name
? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/{{#([^#]*)#}}/g, (a, b) => {
const arr: string[] = b.split('.')
if (isSystemVar(arr))
return `{{${b}}}`
return `{{${arr.slice(1).join('.')}}}`
})
: ''
}
return ''
}, [isSelect, t, value])
return (
<div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
@@ -65,7 +88,7 @@ const ConditionValue = ({
</div>
{
!notHasValue && (
<div className='truncate text-xs text-text-secondary' title={formatValue}>{formatValue}</div>
<div className='truncate text-xs text-text-secondary' title={formatValue}>{isSelect ? selectName : formatValue}</div>
)
}
</div>

View File

@@ -0,0 +1,225 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ReactSortable } from 'react-sortablejs'
import {
RiAddLine,
RiDeleteBinLine,
RiDraggable,
} from '@remixicon/react'
import type { CaseItem, HandleAddCondition, HandleAddSubVariableCondition, HandleRemoveCondition, HandleToggleConditionLogicalOperator, HandleToggleSubVariableConditionLogicalOperator, HandleUpdateCondition, HandleUpdateSubVariableCondition, handleRemoveSubVariableCondition } from '../types'
import type { Node, NodeOutPutVar, Var } from '../../../types'
import { VarType } from '../../../types'
import { useGetAvailableVars } from '../../variable-assigner/hooks'
import { SUB_VARIABLES } from '../default'
import ConditionList from './condition-list'
import ConditionAdd from './condition-add'
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
caseId?: string
conditionId?: string
cases: CaseItem[]
readOnly: boolean
handleSortCase?: (sortedCases: (CaseItem & { id: string })[]) => void
handleRemoveCase?: (caseId: string) => void
handleAddCondition?: HandleAddCondition
handleRemoveCondition?: HandleRemoveCondition
handleUpdateCondition?: HandleUpdateCondition
handleToggleConditionLogicalOperator?: HandleToggleConditionLogicalOperator
handleAddSubVariableCondition?: HandleAddSubVariableCondition
handleRemoveSubVariableCondition?: handleRemoveSubVariableCondition
handleUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
handleToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
nodeId: string
nodesOutputVars: NodeOutPutVar[]
availableNodes: Node[]
varsIsVarFileAttribute?: Record<string, boolean>
filterVar: (varPayload: Var) => boolean
}
const ConditionWrap: FC<Props> = ({
isSubVariable,
caseId,
conditionId,
nodeId: id = '',
cases = [],
readOnly,
handleSortCase = () => { },
handleRemoveCase,
handleUpdateCondition,
handleAddCondition,
handleRemoveCondition,
handleToggleConditionLogicalOperator,
handleAddSubVariableCondition,
handleRemoveSubVariableCondition,
handleUpdateSubVariableCondition,
handleToggleSubVariableConditionLogicalOperator,
nodesOutputVars = [],
availableNodes = [],
varsIsVarFileAttribute = {},
filterVar = () => true,
}) => {
const { t } = useTranslation()
const getAvailableVars = useGetAvailableVars()
const [willDeleteCaseId, setWillDeleteCaseId] = useState('')
const casesLength = cases.length
const filterNumberVar = useCallback((varPayload: Var) => {
return varPayload.type === VarType.number
}, [])
const subVarOptions = SUB_VARIABLES.map(item => ({
name: item,
value: item,
}))
return (
<>
<ReactSortable
list={cases.map(caseItem => ({ ...caseItem, id: caseItem.case_id }))}
setList={handleSortCase}
handle='.handle'
ghostClass='bg-components-panel-bg'
animation={150}
disabled={readOnly || isSubVariable}
>
{
cases.map((item, index) => (
<div key={item.case_id}>
<div
className={cn(
'group relative rounded-[10px] bg-components-panel-bg',
willDeleteCaseId === item.case_id && 'bg-state-destructive-hover',
!isSubVariable && 'py-1 px-3 min-h-[40px] ',
isSubVariable && 'px-1 py-2',
)}
>
{!isSubVariable && (
<>
<RiDraggable className={cn(
'hidden handle absolute top-2 left-1 w-3 h-3 text-text-quaternary cursor-pointer',
casesLength > 1 && 'group-hover:block',
)} />
<div className={cn(
'absolute left-4 leading-4 text-[13px] font-semibold text-text-secondary',
casesLength === 1 ? 'top-2.5' : 'top-1',
)}>
{
index === 0 ? 'IF' : 'ELIF'
}
{
casesLength > 1 && (
<div className='text-[10px] text-text-tertiary font-medium'>CASE {index + 1}</div>
)
}
</div>
</>
)}
{
!!item.conditions.length && (
<div className='mb-2'>
<ConditionList
disabled={readOnly}
caseItem={item}
caseId={isSubVariable ? caseId! : item.case_id}
conditionId={conditionId}
onUpdateCondition={handleUpdateCondition}
onRemoveCondition={handleRemoveCondition}
onToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
nodeId={id}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
filterVar={filterVar}
numberVariables={getAvailableVars(id, '', filterNumberVar)}
varsIsVarFileAttribute={varsIsVarFileAttribute}
onAddSubVariableCondition={handleAddSubVariableCondition}
onRemoveSubVariableCondition={handleRemoveSubVariableCondition}
onUpdateSubVariableCondition={handleUpdateSubVariableCondition}
onToggleSubVariableConditionLogicalOperator={handleToggleSubVariableConditionLogicalOperator}
isSubVariable={isSubVariable}
/>
</div>
)
}
<div className={cn(
'flex items-center justify-between pr-[30px]',
!item.conditions.length && !isSubVariable && 'mt-1',
!item.conditions.length && isSubVariable && 'mt-2',
!isSubVariable && ' pl-[60px]',
)}>
{isSubVariable
? (
<Select
popupInnerClassName='w-[165px] max-h-none'
onSelect={value => handleAddSubVariableCondition?.(caseId!, conditionId!, value.value as string)}
items={subVarOptions}
value=''
renderTrigger={() => (
<Button
size='small'
disabled={readOnly}
>
<RiAddLine className='mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.ifElse.addSubVariable')}
</Button>
)}
hideChecked
/>
)
: (
<ConditionAdd
disabled={readOnly}
caseId={item.case_id}
variables={getAvailableVars(id, '', filterVar)}
onSelectVariable={handleAddCondition!}
/>
)}
{
((index === 0 && casesLength > 1) || (index > 0)) && (
<Button
className='hover:text-components-button-destructive-ghost-text hover:bg-components-button-destructive-ghost-bg-hover'
size='small'
variant='ghost'
disabled={readOnly}
onClick={() => handleRemoveCase?.(item.case_id)}
onMouseEnter={() => setWillDeleteCaseId(item.case_id)}
onMouseLeave={() => setWillDeleteCaseId('')}
>
<RiDeleteBinLine className='mr-1 w-3.5 h-3.5' />
{t('common.operation.remove')}
</Button>
)
}
</div>
</div>
{!isSubVariable && (
<div className='my-2 mx-3 h-[1px] bg-divider-subtle'></div>
)}
</div>
))
}
</ReactSortable>
{(cases.length === 0) && (
<Button
size='small'
disabled={readOnly}
onClick={() => handleAddSubVariableCondition?.(caseId!, conditionId!)}
>
<RiAddLine className='mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.ifElse.addSubVariable')}
</Button>
)}
</>
)
}
export default React.memo(ConditionWrap)

View File

@@ -1,6 +1,7 @@
import { BlockEnum, type NodeDefault } from '../../types'
import { type IfElseNodeType, LogicalOperator } from './types'
import { isEmptyRelatedOperator } from './utils'
import { TransferMethod } from '@/types/app'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'
@@ -49,8 +50,25 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
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 && !isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
if (!errorMessages) {
if (condition.sub_variable_condition) {
const isSet = condition.sub_variable_condition.conditions.every((c) => {
if (!c.comparison_operator)
return false
if (isEmptyRelatedOperator(c.comparison_operator!))
return true
return !!c.value
})
if (!isSet)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
}
else {
if (!isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
}
}
})
})
return {
@@ -61,3 +79,18 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
}
export default nodeDefault
export const FILE_TYPE_OPTIONS = [
{ value: 'image', i18nKey: 'image' },
{ value: 'document', i18nKey: 'doc' },
{ value: 'audio', i18nKey: 'audio' },
{ value: 'video', i18nKey: 'video' },
]
export const TRANSFER_METHOD = [
{ value: TransferMethod.local_file, i18nKey: 'localUpload' },
{ value: TransferMethod.remote_url, i18nKey: 'url' },
]
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')

View File

@@ -1,11 +1,12 @@
import type { FC } from 'react'
import React from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { NodeProps } from 'reactflow'
import { NodeSourceHandle } from '../_base/components/node-handle'
import { isEmptyRelatedOperator } from './utils'
import type { IfElseNodeType } from './types'
import type { Condition, IfElseNodeType } from './types'
import ConditionValue from './components/condition-value'
import ConditionFilesListValue from './components/condition-files-list-value'
const i18nPrefix = 'workflow.nodes.ifElse'
const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
@@ -13,6 +14,32 @@ const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
const { t } = useTranslation()
const { cases } = data
const casesLength = cases.length
const checkIsConditionSet = useCallback((condition: Condition) => {
if (!condition.variable_selector || condition.variable_selector.length === 0)
return false
if (condition.sub_variable_condition) {
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
})
return isSet
}
else {
if (isEmptyRelatedOperator(condition.comparison_operator!))
return true
return !!condition.value
}
}, [])
const conditionNotSet = (<div className='flex items-center h-6 px-1 space-x-1 text-xs font-normal text-text-secondary bg-workflow-block-parma-bg rounded-md'>
{t(`${i18nPrefix}.conditionNotSetup`)}
</div>)
return (
<div className='px-3'>
@@ -35,21 +62,25 @@ const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
<div className='space-y-0.5'>
{caseItem.conditions.map((condition, i) => (
<div key={condition.id} className='relative'>
{(condition.variable_selector?.length > 0 && condition.comparison_operator && (isEmptyRelatedOperator(condition.comparison_operator!) ? true : !!condition.value))
? (
<ConditionValue
variableSelector={condition.variable_selector}
operator={condition.comparison_operator}
value={condition.value}
/>
)
: (
<div className='flex items-center h-6 px-1 space-x-1 text-xs font-normal text-text-secondary bg-workflow-block-parma-bg rounded-md'>
{t(`${i18nPrefix}.conditionNotSetup`)}
</div>
)}
{
checkIsConditionSet(condition)
? (
(!isEmptyRelatedOperator(condition.comparison_operator!) && condition.sub_variable_condition)
? (
<ConditionFilesListValue condition={condition} />
)
: (
<ConditionValue
variableSelector={condition.variable_selector!}
operator={condition.comparison_operator!}
value={condition.value}
/>
)
)
: conditionNotSet}
{i !== caseItem.conditions.length - 1 && (
<div className='absolute z-10 right-0 bottom-[-10px] leading-4 text-[10px] font-medium text-text-accent uppercase'>{t(`${i18nPrefix}.${caseItem.logical_operator}`)}</div>
<div className='absolute z-10 right-1 bottom-[-10px] leading-4 text-[10px] font-medium text-text-accent uppercase'>{t(`${i18nPrefix}.${caseItem.logical_operator}`)}</div>
)}
</div>
))}

View File

@@ -1,24 +1,18 @@
import type { FC } from 'react'
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { ReactSortable } from 'react-sortablejs'
import {
RiAddLine,
RiDeleteBinLine,
RiDraggable,
} from '@remixicon/react'
import useConfig from './use-config'
import ConditionAdd from './components/condition-add'
import ConditionList from './components/condition-list'
import type { IfElseNodeType } from './types'
import ConditionWrap from './components/condition-wrap'
import Button from '@/app/components/base/button'
import type { NodePanelProps } from '@/app/components/workflow/types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import { useGetAvailableVars } from '@/app/components/workflow/nodes/variable-assigner/hooks'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.ifElse'
const Panel: FC<NodePanelProps<IfElseNodeType>> = ({
@@ -26,110 +20,48 @@ const Panel: FC<NodePanelProps<IfElseNodeType>> = ({
data,
}) => {
const { t } = useTranslation()
const getAvailableVars = useGetAvailableVars()
const {
readOnly,
inputs,
filterVar,
filterNumberVar,
handleAddCase,
handleRemoveCase,
handleSortCase,
handleAddCondition,
handleUpdateCondition,
handleRemoveCondition,
handleUpdateConditionLogicalOperator,
handleToggleConditionLogicalOperator,
handleAddSubVariableCondition,
handleRemoveSubVariableCondition,
handleUpdateSubVariableCondition,
handleToggleSubVariableConditionLogicalOperator,
nodesOutputVars,
availableNodes,
varsIsVarFileAttribute,
} = useConfig(id, data)
const [willDeleteCaseId, setWillDeleteCaseId] = useState('')
const cases = inputs.cases || []
const casesLength = cases.length
return (
<div className='p-1'>
<ReactSortable
list={cases.map(caseItem => ({ ...caseItem, id: caseItem.case_id }))}
setList={handleSortCase}
handle='.handle'
ghostClass='bg-components-panel-bg'
animation={150}
>
{
cases.map((item, index) => (
<div key={item.case_id}>
<div
className={cn(
'group relative py-1 px-3 min-h-[40px] rounded-[10px] bg-components-panel-bg',
willDeleteCaseId === item.case_id && 'bg-state-destructive-hover',
)}
>
<RiDraggable className={cn(
'hidden handle absolute top-2 left-1 w-3 h-3 text-text-quaternary cursor-pointer',
casesLength > 1 && 'group-hover:block',
)} />
<div className={cn(
'absolute left-4 leading-4 text-[13px] font-semibold text-text-secondary',
casesLength === 1 ? 'top-2.5' : 'top-1',
)}>
{
index === 0 ? 'IF' : 'ELIF'
}
{
casesLength > 1 && (
<div className='text-[10px] text-text-tertiary font-medium'>CASE {index + 1}</div>
)
}
</div>
{
!!item.conditions.length && (
<div className='mb-2'>
<ConditionList
disabled={readOnly}
caseItem={item}
onUpdateCondition={handleUpdateCondition}
onRemoveCondition={handleRemoveCondition}
onUpdateConditionLogicalOperator={handleUpdateConditionLogicalOperator}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
numberVariables={getAvailableVars(id, '', filterNumberVar)}
/>
</div>
)
}
<div className={cn(
'flex items-center justify-between pl-[60px] pr-[30px]',
!item.conditions.length && 'mt-1',
)}>
<ConditionAdd
disabled={readOnly}
caseId={item.case_id}
variables={getAvailableVars(id, '', filterVar)}
onSelectVariable={handleAddCondition}
/>
{
((index === 0 && casesLength > 1) || (index > 0)) && (
<Button
className='hover:text-components-button-destructive-ghost-text hover:bg-components-button-destructive-ghost-bg-hover'
size='small'
variant='ghost'
disabled={readOnly}
onClick={() => handleRemoveCase(item.case_id)}
onMouseEnter={() => setWillDeleteCaseId(item.case_id)}
onMouseLeave={() => setWillDeleteCaseId('')}
>
<RiDeleteBinLine className='mr-1 w-3.5 h-3.5' />
{t('common.operation.remove')}
</Button>
)
}
</div>
</div>
<div className='my-2 mx-3 h-[1px] bg-divider-subtle'></div>
</div>
))
}
</ReactSortable>
<ConditionWrap
nodeId={id}
cases={cases}
readOnly={readOnly}
handleSortCase={handleSortCase}
handleRemoveCase={handleRemoveCase}
handleAddCondition={handleAddCondition}
handleRemoveCondition={handleRemoveCondition}
handleUpdateCondition={handleUpdateCondition}
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
handleAddSubVariableCondition={handleAddSubVariableCondition}
handleRemoveSubVariableCondition={handleRemoveSubVariableCondition}
handleUpdateSubVariableCondition={handleUpdateSubVariableCondition}
handleToggleSubVariableConditionLogicalOperator={handleToggleSubVariableConditionLogicalOperator}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
varsIsVarFileAttribute={varsIsVarFileAttribute}
filterVar={filterVar}
/>
<div className='px-4 py-2'>
<Button
className='w-full'

View File

@@ -28,15 +28,22 @@ export enum ComparisonOperator {
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
variable_selector?: ValueSelector
key?: string // sub variable key
comparison_operator?: ComparisonOperator
value: string
value: string | string[]
numberVarType?: NumberVarType
sub_variable_condition?: CaseItem
}
export type CaseItem = {
@@ -49,9 +56,15 @@ export type IfElseNodeType = CommonNodeType & {
logical_operator?: LogicalOperator
conditions?: Condition[]
cases: CaseItem[]
isInIteration: boolean
}
export type HandleAddCondition = (caseId: string, valueSelector: ValueSelector, varItem: Var) => void
export type HandleRemoveCondition = (caseId: string, conditionId: string) => void
export type HandleUpdateCondition = (caseId: string, conditionId: string, newCondition: Condition) => void
export type HandleUpdateConditionLogicalOperator = (caseId: string, value: LogicalOperator) => void
export type HandleToggleConditionLogicalOperator = (caseId: string) => void
export type HandleAddSubVariableCondition = (caseId: string, conditionId: string, key?: string) => void
export type handleRemoveSubVariableCondition = (caseId: string, conditionId: string, subConditionId: string) => void
export type HandleUpdateSubVariableCondition = (caseId: string, conditionId: string, subConditionId: string, newSubCondition: Condition) => void
export type HandleToggleSubVariableConditionLogicalOperator = (caseId: string, conditionId: string) => void

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import produce from 'immer'
import { v4 as uuid4 } from 'uuid'
import { useUpdateNodeInternals } from 'reactflow'
@@ -10,15 +10,19 @@ import { LogicalOperator } from './types'
import type {
CaseItem,
HandleAddCondition,
HandleAddSubVariableCondition,
HandleRemoveCondition,
HandleToggleConditionLogicalOperator,
HandleToggleSubVariableConditionLogicalOperator,
HandleUpdateCondition,
HandleUpdateConditionLogicalOperator,
HandleUpdateSubVariableCondition,
IfElseNodeType,
} from './types'
import {
branchNameCorrect,
getOperators,
} from './utils'
import useIsVarFileAttribute from './use-is-var-file-attribute'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import {
useEdgesInteractions,
@@ -32,8 +36,8 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
const { inputs, setInputs } = useNodeCrud<IfElseNodeType>(id, payload)
const filterVar = useCallback((varPayload: Var) => {
return varPayload.type !== VarType.arrayFile
const filterVar = useCallback(() => {
return true
}, [])
const {
@@ -48,6 +52,23 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
return varPayload.type === VarType.number
}, [])
const {
getIsVarFileAttribute,
} = useIsVarFileAttribute({
nodeId: id,
isInIteration: payload.isInIteration,
})
const varsIsVarFileAttribute = useMemo(() => {
const conditions: Record<string, boolean> = {}
inputs.cases?.forEach((c) => {
c.conditions.forEach((condition) => {
conditions[condition.id] = getIsVarFileAttribute(condition.variable_selector!)
})
})
return conditions
}, [inputs.cases, getIsVarFileAttribute])
const {
availableVars: availableNumberVars,
availableNodesWithParent: availableNumberNodesWithParent,
@@ -121,13 +142,13 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
id: uuid4(),
varType: varItem.type,
variable_selector: valueSelector,
comparison_operator: getOperators(varItem.type)[0],
comparison_operator: getOperators(varItem.type, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
value: '',
})
}
})
setInputs(newInputs)
}, [inputs, setInputs])
}, [getIsVarFileAttribute, inputs, setInputs])
const handleRemoveCondition = useCallback<HandleRemoveCondition>((caseId, conditionId) => {
const newInputs = produce(inputs, (draft) => {
@@ -150,11 +171,81 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
setInputs(newInputs)
}, [inputs, setInputs])
const handleUpdateConditionLogicalOperator = useCallback<HandleUpdateConditionLogicalOperator>((caseId, value) => {
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>((caseId) => {
const newInputs = produce(inputs, (draft) => {
const targetCase = draft.cases?.find(item => item.case_id === caseId)
if (targetCase)
targetCase.logical_operator = value
targetCase.logical_operator = targetCase.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleAddSubVariableCondition = useCallback<HandleAddSubVariableCondition>((caseId: string, conditionId: string, key?: string) => {
const newInputs = produce(inputs, (draft) => {
const condition = draft.cases?.find(item => item.case_id === caseId)?.conditions.find(item => item.id === conditionId)
if (!condition)
return
if (!condition?.sub_variable_condition) {
condition.sub_variable_condition = {
case_id: uuid4(),
logical_operator: LogicalOperator.and,
conditions: [],
}
}
const subVarCondition = condition.sub_variable_condition
if (subVarCondition) {
if (!subVarCondition.conditions)
subVarCondition.conditions = []
subVarCondition.conditions.push({
id: uuid4(),
key: key || '',
varType: VarType.string,
comparison_operator: undefined,
value: '',
})
}
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleRemoveSubVariableCondition = useCallback((caseId: string, conditionId: string, subConditionId: string) => {
const newInputs = produce(inputs, (draft) => {
const condition = draft.cases?.find(item => item.case_id === caseId)?.conditions.find(item => item.id === conditionId)
if (!condition)
return
if (!condition?.sub_variable_condition)
return
const subVarCondition = condition.sub_variable_condition
if (subVarCondition)
subVarCondition.conditions = subVarCondition.conditions.filter(item => item.id !== subConditionId)
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleUpdateSubVariableCondition = useCallback<HandleUpdateSubVariableCondition>((caseId, conditionId, subConditionId, newSubCondition) => {
const newInputs = produce(inputs, (draft) => {
const targetCase = draft.cases?.find(item => item.case_id === caseId)
if (targetCase) {
const targetCondition = targetCase.conditions.find(item => item.id === conditionId)
if (targetCondition && targetCondition.sub_variable_condition) {
const targetSubCondition = targetCondition.sub_variable_condition.conditions.find(item => item.id === subConditionId)
if (targetSubCondition)
Object.assign(targetSubCondition, newSubCondition)
}
}
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleToggleSubVariableConditionLogicalOperator = useCallback<HandleToggleSubVariableConditionLogicalOperator>((caseId, conditionId) => {
const newInputs = produce(inputs, (draft) => {
const targetCase = draft.cases?.find(item => item.case_id === caseId)
if (targetCase) {
const targetCondition = targetCase.conditions.find(item => item.id === conditionId)
if (targetCondition && targetCondition.sub_variable_condition)
targetCondition.sub_variable_condition.logical_operator = targetCondition.sub_variable_condition.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
}
})
setInputs(newInputs)
}, [inputs, setInputs])
@@ -170,11 +261,16 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
handleAddCondition,
handleRemoveCondition,
handleUpdateCondition,
handleUpdateConditionLogicalOperator,
handleToggleConditionLogicalOperator,
handleAddSubVariableCondition,
handleUpdateSubVariableCondition,
handleRemoveSubVariableCondition,
handleToggleSubVariableConditionLogicalOperator,
nodesOutputVars: availableVars,
availableNodes: availableNodesWithParent,
nodesOutputNumberVars: availableNumberVars,
availableNumberNodes: availableNumberNodesWithParent,
varsIsVarFileAttribute,
}
}

View File

@@ -0,0 +1,45 @@
import { useStoreApi } from 'reactflow'
import { useMemo } from 'react'
import { useIsChatMode, useWorkflow, useWorkflowVariables } from '../../hooks'
import type { ValueSelector } from '../../types'
import { VarType } from '../../types'
type Params = {
nodeId: string
isInIteration: boolean
}
const useIsVarFileAttribute = ({
nodeId,
isInIteration,
}: Params) => {
const isChatMode = useIsChatMode()
const store = useStoreApi()
const { getBeforeNodesInSameBranch } = useWorkflow()
const {
getNodes,
} = store.getState()
const currentNode = getNodes().find(n => n.id === nodeId)
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
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({
parentNode: iterationNode,
valueSelector: parentVariable,
availableNodes,
isChatMode,
isConstant: false,
})
return varType === VarType.file
}
return {
getIsVarFileAttribute,
}
}
export default useIsVarFileAttribute

View File

@@ -18,7 +18,72 @@ export const isComparisonOperatorNeedTranslate = (operator?: ComparisonOperator)
return !notTranslateKey.includes(operator)
}
export const getOperators = (type?: VarType) => {
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 [
@@ -42,6 +107,11 @@ export const getOperators = (type?: VarType) => {
ComparisonOperator.empty,
ComparisonOperator.notEmpty,
]
case VarType.file:
return [
ComparisonOperator.exists,
ComparisonOperator.notExists,
]
case VarType.arrayString:
case VarType.arrayNumber:
return [
@@ -56,6 +126,14 @@ export const getOperators = (type?: VarType) => {
ComparisonOperator.empty,
ComparisonOperator.notEmpty,
]
case VarType.arrayFile:
return [
ComparisonOperator.contains,
ComparisonOperator.notContains,
ComparisonOperator.allOf,
ComparisonOperator.empty,
ComparisonOperator.notEmpty,
]
default:
return [
ComparisonOperator.is,
@@ -70,7 +148,7 @@ export const comparisonOperatorNotRequireValue = (operator?: ComparisonOperator)
if (!operator)
return false
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull].includes(operator)
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull, ComparisonOperator.exists, ComparisonOperator.notExists].includes(operator)
}
export const branchNameCorrect = (branches: Branch[]) => {