| @@ -40,4 +40,8 @@ | |||
| .btn-ghost { | |||
| @apply bg-transparent hover:bg-gray-200 border-transparent shadow-none text-gray-700; | |||
| } | |||
| .btn-tertiary { | |||
| @apply bg-[#F2F4F7] hover:bg-[#E9EBF0] border-transparent shadow-none text-gray-700; | |||
| } | |||
| } | |||
| @@ -14,6 +14,7 @@ const buttonVariants = cva( | |||
| 'secondary': 'btn-secondary', | |||
| 'secondary-accent': 'btn-secondary-accent', | |||
| 'ghost': 'btn-ghost', | |||
| 'tertiary': 'btn-tertiary', | |||
| }, | |||
| size: { | |||
| small: 'btn-small', | |||
| @@ -88,7 +88,7 @@ const WorkflowVariableBlockComponent = ({ | |||
| </div> | |||
| ) | |||
| } | |||
| <div className='shrink-0 mx-0.5 text-xs font-medium text-gray-500 truncate' title={node?.title} style={{ | |||
| <div className='shrink-0 mx-0.5 max-w-[60px] text-xs font-medium text-gray-500 truncate' title={node?.title} style={{ | |||
| }}>{node?.title}</div> | |||
| <Line3 className='mr-0.5 text-gray-300'></Line3> | |||
| </div> | |||
| @@ -360,7 +360,7 @@ export const HTTP_REQUEST_OUTPUT_STRUCT: Var[] = [ | |||
| }, | |||
| { | |||
| variable: 'headers', | |||
| type: VarType.string, | |||
| type: VarType.object, | |||
| }, | |||
| { | |||
| variable: 'files', | |||
| @@ -21,6 +21,7 @@ import ReactFlow, { | |||
| useNodesState, | |||
| useOnViewportChange, | |||
| useReactFlow, | |||
| useStoreApi, | |||
| } from 'reactflow' | |||
| import type { | |||
| Viewport, | |||
| @@ -278,6 +279,15 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| { exactMatch: true, useCapture: true }, | |||
| ) | |||
| const store = useStoreApi() | |||
| if (process.env.NODE_ENV === 'development') { | |||
| store.getState().onError = (code, message) => { | |||
| if (code === '002') | |||
| return | |||
| console.warn(message) | |||
| } | |||
| } | |||
| return ( | |||
| <div | |||
| id='workflow-container' | |||
| @@ -1,6 +1,7 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import cn from 'classnames' | |||
| import { useWorkflow } from '../../../hooks' | |||
| import { BlockEnum } from '../../../types' | |||
| import { VarBlockIcon } from '../../../block-icon' | |||
| @@ -10,6 +11,7 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop | |||
| type Props = { | |||
| nodeId: string | |||
| value: string | |||
| className?: string | |||
| } | |||
| const VAR_PLACEHOLDER = '@#!@#!' | |||
| @@ -17,6 +19,7 @@ const VAR_PLACEHOLDER = '@#!@#!' | |||
| const ReadonlyInputWithSelectVar: FC<Props> = ({ | |||
| nodeId, | |||
| value, | |||
| className, | |||
| }) => { | |||
| const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow() | |||
| const availableNodes = getBeforeNodesInSameBranchIncludeParent(nodeId) | |||
| @@ -64,7 +67,7 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({ | |||
| })() | |||
| return ( | |||
| <div className='break-all text-xs'> | |||
| <div className={cn('break-all text-xs', className)}> | |||
| {res} | |||
| </div> | |||
| ) | |||
| @@ -0,0 +1,66 @@ | |||
| import { useMemo } from 'react' | |||
| import { useNodes } from 'reactflow' | |||
| import { capitalize } from 'lodash-es' | |||
| import { VarBlockIcon } from '@/app/components/workflow/block-icon' | |||
| import type { | |||
| CommonNodeType, | |||
| ValueSelector, | |||
| VarType, | |||
| } from '@/app/components/workflow/types' | |||
| import { BlockEnum } from '@/app/components/workflow/types' | |||
| import { Line3 } from '@/app/components/base/icons/src/public/common' | |||
| import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' | |||
| import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' | |||
| type VariableTagProps = { | |||
| valueSelector: ValueSelector | |||
| varType: VarType | |||
| } | |||
| const VariableTag = ({ | |||
| valueSelector, | |||
| varType, | |||
| }: VariableTagProps) => { | |||
| const nodes = useNodes<CommonNodeType>() | |||
| const node = useMemo(() => { | |||
| if (isSystemVar(valueSelector)) | |||
| return nodes.find(node => node.data.type === BlockEnum.Start) | |||
| return nodes.find(node => node.id === valueSelector[0]) | |||
| }, [nodes, valueSelector]) | |||
| const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.') | |||
| return ( | |||
| <div className='inline-flex items-center px-1.5 max-w-full h-6 text-xs rounded-md border-[0.5px] border-[rgba(16, 2440,0.08)] bg-white shadow-xs'> | |||
| { | |||
| node && ( | |||
| <VarBlockIcon | |||
| className='shrink-0 mr-0.5 text-[#354052]' | |||
| type={node!.data.type} | |||
| /> | |||
| ) | |||
| } | |||
| <div | |||
| className='max-w-[60px] truncate text-[#354052] font-medium' | |||
| title={node?.data.title} | |||
| > | |||
| {node?.data.title} | |||
| </div> | |||
| <Line3 className='shrink-0 mx-0.5' /> | |||
| <Variable02 className='shrink-0 mr-0.5 w-3.5 h-3.5 text-[#155AEF]' /> | |||
| <div | |||
| className='truncate text-[#155AEF] font-medium' | |||
| title={variableName} | |||
| > | |||
| {variableName} | |||
| </div> | |||
| { | |||
| varType && ( | |||
| <div className='shrink-0 ml-0.5 text-[#676F83]'>{capitalize(varType)}</div> | |||
| ) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| export default VariableTag | |||
| @@ -0,0 +1,75 @@ | |||
| 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 | |||
| caseId: string | |||
| variables: NodeOutPutVar[] | |||
| onSelectVariable: HandleAddCondition | |||
| disabled?: boolean | |||
| } | |||
| const ConditionAdd = ({ | |||
| className, | |||
| caseId, | |||
| variables, | |||
| onSelectVariable, | |||
| disabled, | |||
| }: ConditionAddProps) => { | |||
| const { t } = useTranslation() | |||
| const [open, setOpen] = useState(false) | |||
| const handleSelectVariable = useCallback((valueSelector: ValueSelector, varItem: Var) => { | |||
| onSelectVariable(caseId, valueSelector, varItem) | |||
| setOpen(false) | |||
| }, [caseId, onSelectVariable, setOpen]) | |||
| return ( | |||
| <PortalToFollowElem | |||
| open={open} | |||
| onOpenChange={setOpen} | |||
| placement='bottom-start' | |||
| offset={{ | |||
| mainAxis: 4, | |||
| crossAxis: 0, | |||
| }} | |||
| > | |||
| <PortalToFollowElemTrigger onClick={() => setOpen(!open)}> | |||
| <Button | |||
| size='small' | |||
| className={className} | |||
| disabled={disabled} | |||
| > | |||
| <RiAddLine className='mr-1 w-3.5 h-3.5' /> | |||
| {t('workflow.nodes.ifElse.addCondition')} | |||
| </Button> | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent className='z-[1000]'> | |||
| <div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'> | |||
| <VarReferenceVars | |||
| vars={variables} | |||
| onChange={handleSelectVariable} | |||
| /> | |||
| </div> | |||
| </PortalToFollowElemContent> | |||
| </PortalToFollowElem> | |||
| ) | |||
| } | |||
| export default ConditionAdd | |||
| @@ -1,250 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useCallback, useEffect } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { | |||
| RiDeleteBinLine, | |||
| } from '@remixicon/react' | |||
| import VarReferencePicker from '../../_base/components/variable/var-reference-picker' | |||
| import { isComparisonOperatorNeedTranslate } from '../utils' | |||
| import { VarType } from '../../../types' | |||
| import cn from '@/utils/classnames' | |||
| import type { Condition } from '@/app/components/workflow/nodes/if-else/types' | |||
| import { ComparisonOperator, LogicalOperator } from '@/app/components/workflow/nodes/if-else/types' | |||
| import type { ValueSelector, Var } from '@/app/components/workflow/types' | |||
| import { RefreshCw05 } from '@/app/components/base/icons/src/vender/line/arrows' | |||
| import Selector from '@/app/components/workflow/nodes/_base/components/selector' | |||
| import Toast from '@/app/components/base/toast' | |||
| const i18nPrefix = 'workflow.nodes.ifElse' | |||
| const Line = ( | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="163" height="2" viewBox="0 0 163 2" fill="none"> | |||
| <path d="M0 1H162.5" stroke="url(#paint0_linear_641_36452)" /> | |||
| <defs> | |||
| <linearGradient id="paint0_linear_641_36452" x1="162.5" y1="9.99584" x2="6.6086e-06" y2="9.94317" gradientUnits="userSpaceOnUse"> | |||
| <stop stopColor="#F3F4F6" /> | |||
| <stop offset="1" stopColor="#F3F4F6" stopOpacity="0" /> | |||
| </linearGradient> | |||
| </defs> | |||
| </svg> | |||
| ) | |||
| const getOperators = (type?: VarType) => { | |||
| 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.is, | |||
| ComparisonOperator.isNot, | |||
| ComparisonOperator.empty, | |||
| ComparisonOperator.notEmpty, | |||
| ] | |||
| 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, | |||
| ] | |||
| default: | |||
| return [ | |||
| ComparisonOperator.is, | |||
| ComparisonOperator.isNot, | |||
| ComparisonOperator.empty, | |||
| ComparisonOperator.notEmpty, | |||
| ] | |||
| } | |||
| } | |||
| type ItemProps = { | |||
| readonly: boolean | |||
| nodeId: string | |||
| payload: Condition | |||
| varType?: VarType | |||
| onChange: (newItem: Condition) => void | |||
| canRemove: boolean | |||
| onRemove?: () => void | |||
| isShowLogicalOperator?: boolean | |||
| logicalOperator: LogicalOperator | |||
| onLogicalOperatorToggle: () => void | |||
| filterVar: (varPayload: Var) => boolean | |||
| } | |||
| const Item: FC<ItemProps> = ({ | |||
| readonly, | |||
| nodeId, | |||
| payload, | |||
| varType = VarType.string, | |||
| onChange, | |||
| canRemove, | |||
| onRemove = () => { }, | |||
| isShowLogicalOperator, | |||
| logicalOperator, | |||
| onLogicalOperatorToggle, | |||
| filterVar, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const isValueReadOnly = payload.comparison_operator ? [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull].includes(payload.comparison_operator) : false | |||
| const handleVarReferenceChange = useCallback((value: ValueSelector | string) => { | |||
| onChange({ | |||
| ...payload, | |||
| variable_selector: value as ValueSelector, | |||
| }) | |||
| }, [onChange, payload]) | |||
| // change to default operator if the variable type is changed | |||
| useEffect(() => { | |||
| if (varType && payload.comparison_operator) { | |||
| if (!getOperators(varType).includes(payload.comparison_operator)) { | |||
| onChange({ | |||
| ...payload, | |||
| comparison_operator: getOperators(varType)[0], | |||
| }) | |||
| } | |||
| } | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [varType, payload]) | |||
| const handleValueChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { | |||
| onChange({ | |||
| ...payload, | |||
| value: e.target.value, | |||
| }) | |||
| }, [onChange, payload]) | |||
| const handleComparisonOperatorChange = useCallback((v: ComparisonOperator) => { | |||
| onChange({ | |||
| ...payload, | |||
| comparison_operator: v, | |||
| }) | |||
| }, [onChange, payload]) | |||
| return ( | |||
| <div className='space-y-2'> | |||
| {isShowLogicalOperator && ( | |||
| <div className='flex items-center justify-center select-none'> | |||
| <div className='flex items-center '> | |||
| {Line} | |||
| <div | |||
| className='shrink-0 mx-1 flex items-center h-[22px] pl-2 pr-1.5 border border-gray-200 rounded-lg bg-white shadow-xs space-x-0.5 text-primary-600 cursor-pointer' | |||
| onClick={onLogicalOperatorToggle} | |||
| > | |||
| <div className='text-xs font-semibold uppercase'>{t(`${i18nPrefix}.${logicalOperator === LogicalOperator.and ? 'and' : 'or'}`)}</div> | |||
| <RefreshCw05 className='w-3 h-3' /> | |||
| </div> | |||
| <div className=' rotate-180'> | |||
| {Line} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| <div className='flex items-center space-x-1'> | |||
| <VarReferencePicker | |||
| nodeId={nodeId} | |||
| readonly={readonly} | |||
| isShowNodeName | |||
| className='min-w-[162px] flex-grow' | |||
| value={payload.variable_selector} | |||
| onChange={handleVarReferenceChange} | |||
| filterVar={filterVar} | |||
| /> | |||
| <Selector | |||
| popupClassName='top-[34px]' | |||
| itemClassName='capitalize' | |||
| trigger={ | |||
| <div | |||
| onClick={(e) => { | |||
| if (readonly) { | |||
| e.stopPropagation() | |||
| return | |||
| } | |||
| if (!payload.variable_selector || payload.variable_selector.length === 0) { | |||
| e.stopPropagation() | |||
| Toast.notify({ | |||
| message: t(`${i18nPrefix}.notSetVariable`), | |||
| type: 'error', | |||
| }) | |||
| } | |||
| }} | |||
| className={cn(!readonly && 'cursor-pointer', 'shrink-0 w-[100px] whitespace-nowrap flex items-center h-8 justify-between px-2.5 rounded-lg bg-gray-100 capitalize')} | |||
| > | |||
| { | |||
| !payload.comparison_operator | |||
| ? <div className='text-[13px] font-normal text-gray-400'>{t(`${i18nPrefix}.operator`)}</div> | |||
| : <div className='text-[13px] font-normal text-gray-900'>{isComparisonOperatorNeedTranslate(payload.comparison_operator) ? t(`${i18nPrefix}.comparisonOperator.${payload.comparison_operator}`) : payload.comparison_operator}</div> | |||
| } | |||
| </div> | |||
| } | |||
| readonly={readonly} | |||
| value={payload.comparison_operator || ''} | |||
| options={getOperators(varType).map((o) => { | |||
| return { | |||
| label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o, | |||
| value: o, | |||
| } | |||
| })} | |||
| onChange={handleComparisonOperatorChange} | |||
| /> | |||
| <input | |||
| readOnly={readonly || isValueReadOnly || !varType} | |||
| onClick={() => { | |||
| if (readonly) | |||
| return | |||
| if (!varType) { | |||
| Toast.notify({ | |||
| message: t(`${i18nPrefix}.notSetVariable`), | |||
| type: 'error', | |||
| }) | |||
| } | |||
| }} | |||
| value={!isValueReadOnly ? payload.value : ''} | |||
| onChange={handleValueChange} | |||
| placeholder={(!readonly && !isValueReadOnly) ? t(`${i18nPrefix}.enterValue`)! : ''} | |||
| className='min-w-[80px] flex-grow h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200' | |||
| type='text' | |||
| /> | |||
| {!readonly && ( | |||
| <div | |||
| className={cn(canRemove ? 'text-gray-500 bg-gray-100 hover:bg-gray-200 cursor-pointer' : 'bg-gray-25 text-gray-300', 'p-2 rounded-lg ')} | |||
| onClick={canRemove ? onRemove : () => { }} | |||
| > | |||
| <RiDeleteBinLine className='w-4 h-4 ' /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div > | |||
| ) | |||
| } | |||
| export default React.memo(Item) | |||
| @@ -1,91 +0,0 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useCallback } from 'react' | |||
| import produce from 'immer' | |||
| import type { Var, VarType } from '../../../types' | |||
| import Item from './condition-item' | |||
| import cn from '@/utils/classnames' | |||
| import type { Condition, LogicalOperator } from '@/app/components/workflow/nodes/if-else/types' | |||
| type Props = { | |||
| nodeId: string | |||
| className?: string | |||
| readonly: boolean | |||
| list: Condition[] | |||
| varTypesList: (VarType | undefined)[] | |||
| onChange: (newList: Condition[]) => void | |||
| logicalOperator: LogicalOperator | |||
| onLogicalOperatorToggle: () => void | |||
| filterVar: (varPayload: Var) => boolean | |||
| } | |||
| const ConditionList: FC<Props> = ({ | |||
| className, | |||
| readonly, | |||
| nodeId, | |||
| list, | |||
| varTypesList, | |||
| onChange, | |||
| logicalOperator, | |||
| onLogicalOperatorToggle, | |||
| filterVar, | |||
| }) => { | |||
| const handleItemChange = useCallback((index: number) => { | |||
| return (newItem: Condition) => { | |||
| const newList = produce(list, (draft) => { | |||
| draft[index] = newItem | |||
| }) | |||
| onChange(newList) | |||
| } | |||
| }, [list, onChange]) | |||
| const handleItemRemove = useCallback((index: number) => { | |||
| return () => { | |||
| const newList = produce(list, (draft) => { | |||
| draft.splice(index, 1) | |||
| }) | |||
| onChange(newList) | |||
| } | |||
| }, [list, onChange]) | |||
| const canRemove = list.length > 1 | |||
| if (list.length === 0) | |||
| return null | |||
| return ( | |||
| <div className={cn(className, 'space-y-2')}> | |||
| <Item | |||
| readonly={readonly} | |||
| nodeId={nodeId} | |||
| payload={list[0]} | |||
| varType={varTypesList[0]} | |||
| onChange={handleItemChange(0)} | |||
| canRemove={canRemove} | |||
| onRemove={handleItemRemove(0)} | |||
| logicalOperator={logicalOperator} | |||
| onLogicalOperatorToggle={onLogicalOperatorToggle} | |||
| filterVar={filterVar} | |||
| /> | |||
| { | |||
| list.length > 1 && ( | |||
| list.slice(1).map((item, i) => ( | |||
| <Item | |||
| key={item.id} | |||
| readonly={readonly} | |||
| nodeId={nodeId} | |||
| payload={item} | |||
| varType={varTypesList[i + 1]} | |||
| onChange={handleItemChange(i + 1)} | |||
| canRemove={canRemove} | |||
| onRemove={handleItemRemove(i + 1)} | |||
| isShowLogicalOperator | |||
| logicalOperator={logicalOperator} | |||
| onLogicalOperatorToggle={onLogicalOperatorToggle} | |||
| filterVar={filterVar} | |||
| /> | |||
| ))) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(ConditionList) | |||
| @@ -0,0 +1,56 @@ | |||
| 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, | |||
| NodeOutPutVar, | |||
| } from '@/app/components/workflow/types' | |||
| type ConditionInputProps = { | |||
| disabled?: boolean | |||
| value: string | |||
| onChange: (value: string) => void | |||
| nodesOutputVars: NodeOutPutVar[] | |||
| availableNodes: Node[] | |||
| } | |||
| const ConditionInput = ({ | |||
| value, | |||
| onChange, | |||
| disabled, | |||
| nodesOutputVars, | |||
| availableNodes, | |||
| }: ConditionInputProps) => { | |||
| const { t } = useTranslation() | |||
| const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey) | |||
| return ( | |||
| <PromptEditor | |||
| key={controlPromptEditorRerenderKey} | |||
| compact | |||
| value={value} | |||
| placeholder={t('workflow.nodes.ifElse.enterValue') || ''} | |||
| workflowVariableBlock={{ | |||
| show: true, | |||
| variables: nodesOutputVars || [], | |||
| workflowNodesMap: availableNodes.reduce((acc, node) => { | |||
| acc[node.id] = { | |||
| title: node.data.title, | |||
| type: node.data.type, | |||
| } | |||
| if (node.data.type === BlockEnum.Start) { | |||
| acc.sys = { | |||
| title: t('workflow.blocks.start'), | |||
| type: BlockEnum.Start, | |||
| } | |||
| } | |||
| return acc | |||
| }, {} as any), | |||
| }} | |||
| onChange={onChange} | |||
| editable={!disabled} | |||
| /> | |||
| ) | |||
| } | |||
| export default ConditionInput | |||
| @@ -0,0 +1,132 @@ | |||
| import { | |||
| useCallback, | |||
| useState, | |||
| } from 'react' | |||
| import { RiDeleteBinLine } from '@remixicon/react' | |||
| import type { VarType as NumberVarType } from '../../../tool/types' | |||
| import type { | |||
| ComparisonOperator, | |||
| Condition, | |||
| HandleRemoveCondition, | |||
| HandleUpdateCondition, | |||
| } from '../../types' | |||
| import { comparisonOperatorNotRequireValue } from '../../utils' | |||
| import ConditionNumberInput from '../condition-number-input' | |||
| 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, | |||
| } from '@/app/components/workflow/types' | |||
| import { VarType } from '@/app/components/workflow/types' | |||
| import cn from '@/utils/classnames' | |||
| type ConditionItemProps = { | |||
| disabled?: boolean | |||
| caseId: string | |||
| condition: Condition | |||
| onRemoveCondition: HandleRemoveCondition | |||
| onUpdateCondition: HandleUpdateCondition | |||
| nodesOutputVars: NodeOutPutVar[] | |||
| availableNodes: Node[] | |||
| numberVariables: NodeOutPutVar[] | |||
| } | |||
| const ConditionItem = ({ | |||
| disabled, | |||
| caseId, | |||
| condition, | |||
| onRemoveCondition, | |||
| onUpdateCondition, | |||
| nodesOutputVars, | |||
| availableNodes, | |||
| numberVariables, | |||
| }: ConditionItemProps) => { | |||
| const [isHovered, setIsHovered] = useState(false) | |||
| 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]) | |||
| const handleUpdateConditionNumberVarType = useCallback((numberVarType: NumberVarType) => { | |||
| const newCondition = { | |||
| ...condition, | |||
| numberVarType, | |||
| value: '', | |||
| } | |||
| onUpdateCondition(caseId, condition.id, newCondition) | |||
| }, [caseId, condition, onUpdateCondition]) | |||
| return ( | |||
| <div className='flex mb-1 last-of-type:mb-0'> | |||
| <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} | |||
| /> | |||
| </div> | |||
| <div className='mx-1 w-[1px] h-3 bg-divider-regular'></div> | |||
| <ConditionOperator | |||
| disabled={disabled} | |||
| varType={condition.varType} | |||
| value={condition.comparison_operator} | |||
| onSelect={handleUpdateConditionOperator} | |||
| /> | |||
| </div> | |||
| { | |||
| !comparisonOperatorNotRequireValue(condition.comparison_operator) && 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} | |||
| onChange={handleUpdateConditionValue} | |||
| nodesOutputVars={nodesOutputVars} | |||
| availableNodes={availableNodes} | |||
| /> | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| !comparisonOperatorNotRequireValue(condition.comparison_operator) && 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} | |||
| onValueChange={handleUpdateConditionValue} | |||
| variables={numberVariables} | |||
| /> | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| <div | |||
| className='shrink-0 flex items-center justify-center ml-1 mt-1 w-6 h-6 rounded-lg cursor-pointer hover:bg-state-destructive-hover text-text-tertiary hover:text-text-destructive' | |||
| onMouseEnter={() => setIsHovered(true)} | |||
| onMouseLeave={() => setIsHovered(false)} | |||
| onClick={() => onRemoveCondition(caseId, condition.id)} | |||
| > | |||
| <RiDeleteBinLine className='w-4 h-4' /> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default ConditionItem | |||
| @@ -0,0 +1,91 @@ | |||
| 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 = { | |||
| disabled?: boolean | |||
| varType: VarType | |||
| value?: string | |||
| onSelect: (value: ComparisonOperator) => void | |||
| } | |||
| const ConditionOperator = ({ | |||
| disabled, | |||
| varType, | |||
| value, | |||
| onSelect, | |||
| }: ConditionOperatorProps) => { | |||
| const { t } = useTranslation() | |||
| const [open, setOpen] = useState(false) | |||
| const options = useMemo(() => { | |||
| return getOperators(varType).map((o) => { | |||
| return { | |||
| label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o, | |||
| value: o, | |||
| } | |||
| }) | |||
| }, [t, varType]) | |||
| const selectedOption = options.find(o => 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')} | |||
| size='small' | |||
| variant='ghost' | |||
| disabled={disabled} | |||
| > | |||
| { | |||
| selectedOption | |||
| ? selectedOption.label | |||
| : 'select' | |||
| } | |||
| <RiArrowDownSLine className='ml-1 w-3.5 h-3.5' /> | |||
| </Button> | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent className='z-10'> | |||
| <div className='p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'> | |||
| { | |||
| options.map(option => ( | |||
| <div | |||
| key={option.value} | |||
| className='flex items-center px-3 py-1.5 h-7 text-[13px] font-medium text-text-secondary rounded-lg cursor-pointer hover:bg-state-base-hover' | |||
| onClick={() => { | |||
| onSelect(option.value) | |||
| setOpen(false) | |||
| }} | |||
| > | |||
| {option.label} | |||
| </div> | |||
| )) | |||
| } | |||
| </div> | |||
| </PortalToFollowElemContent> | |||
| </PortalToFollowElem> | |||
| ) | |||
| } | |||
| export default ConditionOperator | |||
| @@ -0,0 +1,75 @@ | |||
| import { RiLoopLeftLine } from '@remixicon/react' | |||
| import { LogicalOperator } from '../../types' | |||
| import type { | |||
| CaseItem, | |||
| HandleRemoveCondition, | |||
| HandleUpdateCondition, | |||
| HandleUpdateConditionLogicalOperator, | |||
| } from '../../types' | |||
| import ConditionItem from './condition-item' | |||
| import type { | |||
| Node, | |||
| NodeOutPutVar, | |||
| } from '@/app/components/workflow/types' | |||
| type ConditionListProps = { | |||
| disabled?: boolean | |||
| caseItem: CaseItem | |||
| onUpdateCondition: HandleUpdateCondition | |||
| onUpdateConditionLogicalOperator: HandleUpdateConditionLogicalOperator | |||
| onRemoveCondition: HandleRemoveCondition | |||
| nodesOutputVars: NodeOutPutVar[] | |||
| availableNodes: Node[] | |||
| numberVariables: NodeOutPutVar[] | |||
| } | |||
| const ConditionList = ({ | |||
| disabled, | |||
| caseItem, | |||
| onUpdateCondition, | |||
| onUpdateConditionLogicalOperator, | |||
| onRemoveCondition, | |||
| nodesOutputVars, | |||
| availableNodes, | |||
| numberVariables, | |||
| }: ConditionListProps) => { | |||
| const { conditions, logical_operator } = caseItem | |||
| return ( | |||
| <div className='relative pl-[60px]'> | |||
| { | |||
| conditions.length > 1 && ( | |||
| <div className='absolute top-0 bottom-0 left-0 w-[60px]'> | |||
| <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) | |||
| }} | |||
| > | |||
| {logical_operator.toUpperCase()} | |||
| <RiLoopLeftLine className='ml-0.5 w-3 h-3' /> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| caseItem.conditions.map(condition => ( | |||
| <ConditionItem | |||
| key={condition.id} | |||
| disabled={disabled} | |||
| caseId={caseItem.case_id} | |||
| condition={condition} | |||
| onUpdateCondition={onUpdateCondition} | |||
| onRemoveCondition={onRemoveCondition} | |||
| nodesOutputVars={nodesOutputVars} | |||
| availableNodes={availableNodes} | |||
| numberVariables={numberVariables} | |||
| /> | |||
| )) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| export default ConditionList | |||
| @@ -0,0 +1,153 @@ | |||
| import { | |||
| memo, | |||
| useCallback, | |||
| useState, | |||
| } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiArrowDownSLine } from '@remixicon/react' | |||
| import { capitalize } from 'lodash-es' | |||
| 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[] | |||
| } | |||
| const ConditionNumberInput = ({ | |||
| numberVarType = NumberVarType.constant, | |||
| onNumberVarTypeChange, | |||
| value, | |||
| onValueChange, | |||
| variables, | |||
| }: ConditionNumberInputProps) => { | |||
| const { t } = useTranslation() | |||
| const [numberVarTypeVisible, setNumberVarTypeVisible] = useState(false) | |||
| const [variableSelectorVisible, setVariableSelectorVisible] = useState(false) | |||
| const handleSelectVariable = useCallback((valueSelector: ValueSelector) => { | |||
| onValueChange(variableTransformer(valueSelector) as string) | |||
| setVariableSelectorVisible(false) | |||
| }, [onValueChange]) | |||
| return ( | |||
| <div className='flex items-center cursor-pointer'> | |||
| <PortalToFollowElem | |||
| open={numberVarTypeVisible} | |||
| onOpenChange={setNumberVarTypeVisible} | |||
| placement='bottom-start' | |||
| offset={{ mainAxis: 2, crossAxis: 0 }} | |||
| > | |||
| <PortalToFollowElemTrigger onClick={() => setNumberVarTypeVisible(v => !v)}> | |||
| <Button | |||
| className='shrink-0' | |||
| variant='ghost' | |||
| size='small' | |||
| > | |||
| {capitalize(numberVarType)} | |||
| <RiArrowDownSLine className='ml-[1px] w-3.5 h-3.5' /> | |||
| </Button> | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent className='z-[1000]'> | |||
| <div className='p-1 w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'> | |||
| { | |||
| options.map(option => ( | |||
| <div | |||
| key={option} | |||
| className={cn( | |||
| 'flex items-center px-3 h-7 rounded-md hover:bg-state-base-hover cursor-pointer', | |||
| 'text-[13px] font-medium text-text-secondary', | |||
| numberVarType === option && 'bg-state-base-hover', | |||
| )} | |||
| onClick={() => { | |||
| onNumberVarTypeChange(option) | |||
| setNumberVarTypeVisible(false) | |||
| }} | |||
| > | |||
| {capitalize(option)} | |||
| </div> | |||
| )) | |||
| } | |||
| </div> | |||
| </PortalToFollowElemContent> | |||
| </PortalToFollowElem> | |||
| <div className='mx-1 w-[1px] h-4 bg-divider-regular'></div> | |||
| <div className='grow w-0 ml-0.5'> | |||
| { | |||
| numberVarType === NumberVarType.variable && ( | |||
| <PortalToFollowElem | |||
| open={variableSelectorVisible} | |||
| onOpenChange={setVariableSelectorVisible} | |||
| placement='bottom-start' | |||
| offset={{ mainAxis: 2, crossAxis: 0 }} | |||
| > | |||
| <PortalToFollowElemTrigger | |||
| className='w-full' | |||
| onClick={() => setVariableSelectorVisible(v => !v)}> | |||
| { | |||
| value && ( | |||
| <VariableTag | |||
| valueSelector={variableTransformer(value) as string[]} | |||
| varType={VarType.number} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| !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')} | |||
| </div> | |||
| ) | |||
| } | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent className='z-[1000]'> | |||
| <div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'> | |||
| <VarReferenceVars | |||
| vars={variables} | |||
| onChange={handleSelectVariable} | |||
| /> | |||
| </div> | |||
| </PortalToFollowElemContent> | |||
| </PortalToFollowElem> | |||
| ) | |||
| } | |||
| { | |||
| 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> | |||
| </div> | |||
| ) | |||
| } | |||
| export default memo(ConditionNumberInput) | |||
| @@ -0,0 +1,70 @@ | |||
| import { | |||
| memo, | |||
| useMemo, | |||
| } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import type { ComparisonOperator } from '../types' | |||
| import { | |||
| comparisonOperatorNotRequireValue, | |||
| isComparisonOperatorNeedTranslate, | |||
| } from '../utils' | |||
| import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' | |||
| import cn from '@/utils/classnames' | |||
| import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' | |||
| type ConditionValueProps = { | |||
| variableSelector: string[] | |||
| operator: ComparisonOperator | |||
| value: string | |||
| } | |||
| const ConditionValue = ({ | |||
| variableSelector, | |||
| operator, | |||
| value, | |||
| }: ConditionValueProps) => { | |||
| const { t } = useTranslation() | |||
| 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 formatValue = useMemo(() => { | |||
| if (notHasValue) | |||
| return '' | |||
| return value.replace(/{{#([^#]*)#}}/g, (a, b) => { | |||
| const arr = b.split('.') | |||
| if (isSystemVar(arr)) | |||
| return `{{${b}}}` | |||
| return `{{${arr.slice(1).join('.')}}}` | |||
| }) | |||
| }, [notHasValue, value]) | |||
| return ( | |||
| <div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'> | |||
| <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' /> | |||
| <div | |||
| className={cn( | |||
| 'shrink-0 truncate text-xs font-medium text-text-accent', | |||
| !notHasValue && 'max-w-[70px]', | |||
| )} | |||
| title={variableName} | |||
| > | |||
| {variableName} | |||
| </div> | |||
| <div | |||
| className='shrink-0 mx-1 text-xs font-medium text-text-primary' | |||
| title={operatorName} | |||
| > | |||
| {operatorName} | |||
| </div> | |||
| { | |||
| !notHasValue && ( | |||
| <div className='truncate text-xs text-text-secondary' title={formatValue}>{formatValue}</div> | |||
| ) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| export default memo(ConditionValue) | |||
| @@ -9,15 +9,20 @@ const nodeDefault: NodeDefault<IfElseNodeType> = { | |||
| _targetBranches: [ | |||
| { | |||
| id: 'true', | |||
| name: 'IS TRUE', | |||
| name: 'IF', | |||
| }, | |||
| { | |||
| id: 'false', | |||
| name: 'IS FALSE', | |||
| name: 'ELSE', | |||
| }, | |||
| ], | |||
| cases: [ | |||
| { | |||
| case_id: 'true', | |||
| logical_operator: LogicalOperator.and, | |||
| conditions: [], | |||
| }, | |||
| ], | |||
| logical_operator: LogicalOperator.and, | |||
| conditions: [], | |||
| }, | |||
| getAvailablePrevNodes(isChatMode: boolean) { | |||
| const nodes = isChatMode | |||
| @@ -31,17 +36,22 @@ const nodeDefault: NodeDefault<IfElseNodeType> = { | |||
| }, | |||
| checkValid(payload: IfElseNodeType, t: any) { | |||
| let errorMessages = '' | |||
| const { conditions } = payload | |||
| if (!conditions || conditions.length === 0) | |||
| const { cases } = payload | |||
| if (!cases || cases.length === 0) | |||
| errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: 'IF' }) | |||
| 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 && !isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value) | |||
| errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) }) | |||
| cases.forEach((caseItem, index) => { | |||
| if (!caseItem.conditions.length) | |||
| errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: index === 0 ? 'IF' : 'ELIF' }) | |||
| caseItem.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 && !isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value) | |||
| errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) }) | |||
| }) | |||
| }) | |||
| return { | |||
| isValid: !errorMessages, | |||
| @@ -3,51 +3,62 @@ import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import type { NodeProps } from 'reactflow' | |||
| import { NodeSourceHandle } from '../_base/components/node-handle' | |||
| import { isComparisonOperatorNeedTranslate, isEmptyRelatedOperator } from './utils' | |||
| import { isEmptyRelatedOperator } from './utils' | |||
| import type { IfElseNodeType } from './types' | |||
| import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' | |||
| import ConditionValue from './components/condition-value' | |||
| const i18nPrefix = 'workflow.nodes.ifElse' | |||
| const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => { | |||
| const { data } = props | |||
| const { t } = useTranslation() | |||
| const { conditions, logical_operator } = data | |||
| const { cases } = data | |||
| const casesLength = cases.length | |||
| return ( | |||
| <div className='px-3'> | |||
| <div className='relative flex items-center h-6 px-1'> | |||
| <div className='w-full text-xs font-semibold text-right text-gray-700'>IF</div> | |||
| <NodeSourceHandle | |||
| {...props} | |||
| handleId='true' | |||
| handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2' | |||
| /> | |||
| </div> | |||
| <div className='space-y-0.5'> | |||
| {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)) | |||
| ? ( | |||
| <div className='flex items-center h-6 px-1 space-x-1 text-xs font-normal text-gray-700 bg-gray-100 rounded-md'> | |||
| <Variable02 className='w-3.5 h-3.5 text-primary-500' /> | |||
| <span>{condition.variable_selector.slice(-1)[0]}</span> | |||
| <span className='text-gray-500'>{isComparisonOperatorNeedTranslate(condition.comparison_operator) ? t(`${i18nPrefix}.comparisonOperator.${condition.comparison_operator}`) : condition.comparison_operator}</span> | |||
| {!isEmptyRelatedOperator(condition.comparison_operator!) && <span>{condition.value}</span>} | |||
| { | |||
| cases.map((caseItem, index) => ( | |||
| <div key={caseItem.case_id}> | |||
| <div className='relative flex items-center h-6 px-1'> | |||
| <div className='flex items-center justify-between w-full'> | |||
| <div className='text-[10px] font-semibold text-text-tertiary'> | |||
| {casesLength > 1 && `CASE ${index + 1}`} | |||
| </div> | |||
| ) | |||
| : ( | |||
| <div className='flex items-center h-6 px-1 space-x-1 text-xs font-normal text-gray-500 bg-gray-100 rounded-md'> | |||
| {t(`${i18nPrefix}.conditionNotSetup`)} | |||
| <div className='text-[12px] font-semibold text-text-secondary'>{index === 0 ? 'IF' : 'ELIF'}</div> | |||
| </div> | |||
| <NodeSourceHandle | |||
| {...props} | |||
| handleId={caseItem.case_id} | |||
| handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2' | |||
| /> | |||
| </div> | |||
| <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> | |||
| )} | |||
| {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> | |||
| )} | |||
| {i !== conditions.length - 1 && ( | |||
| <div className='absolute z-10 right-0 bottom-[-10px] leading-4 text-[10px] font-medium text-primary-600 uppercase'>{t(`${i18nPrefix}.${logical_operator}`)}</div> | |||
| )} | |||
| ))} | |||
| </div> | |||
| </div> | |||
| ))} | |||
| </div> | |||
| )) | |||
| } | |||
| <div className='relative flex items-center h-6 px-1'> | |||
| <div className='w-full text-xs font-semibold text-right text-gray-700'>ELSE</div> | |||
| <div className='w-full text-xs font-semibold text-right text-text-secondary'>ELSE</div> | |||
| <NodeSourceHandle | |||
| {...props} | |||
| handleId='false' | |||
| @@ -1,13 +1,24 @@ | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { | |||
| memo, | |||
| useState, | |||
| } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import Split from '../_base/components/split' | |||
| import AddButton from '../_base/components/add-button' | |||
| 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 Field from '@/app/components/workflow/nodes/_base/components/field' | |||
| 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>> = ({ | |||
| @@ -15,52 +26,130 @@ const Panel: FC<NodePanelProps<IfElseNodeType>> = ({ | |||
| data, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const getAvailableVars = useGetAvailableVars() | |||
| const { | |||
| readOnly, | |||
| inputs, | |||
| handleConditionsChange, | |||
| handleAddCondition, | |||
| handleLogicalOperatorToggle, | |||
| varTypesList, | |||
| filterVar, | |||
| filterNumberVar, | |||
| handleAddCase, | |||
| handleRemoveCase, | |||
| handleSortCase, | |||
| handleAddCondition, | |||
| handleUpdateCondition, | |||
| handleRemoveCondition, | |||
| handleUpdateConditionLogicalOperator, | |||
| nodesOutputVars, | |||
| availableNodes, | |||
| } = useConfig(id, data) | |||
| const [willDeleteCaseId, setWillDeleteCaseId] = useState('') | |||
| const cases = inputs.cases || [] | |||
| const casesLength = cases.length | |||
| return ( | |||
| <div className='mt-2'> | |||
| <div className='px-4 pb-4 space-y-4'> | |||
| <Field | |||
| title={t(`${i18nPrefix}.if`)} | |||
| > | |||
| <> | |||
| <ConditionList | |||
| className='mt-2' | |||
| readonly={readOnly} | |||
| nodeId={id} | |||
| list={inputs.conditions} | |||
| onChange={handleConditionsChange} | |||
| logicalOperator={inputs.logical_operator} | |||
| onLogicalOperatorToggle={handleLogicalOperatorToggle} | |||
| varTypesList={varTypesList} | |||
| filterVar={filterVar} | |||
| /> | |||
| {!readOnly && ( | |||
| <AddButton | |||
| className='mt-3' | |||
| text={t(`${i18nPrefix}.addCondition`)} | |||
| onClick={handleAddCondition} | |||
| /> | |||
| )} | |||
| </> | |||
| </Field> | |||
| <Split /> | |||
| <Field | |||
| title={t(`${i18nPrefix}.else`)} | |||
| <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> | |||
| <div className='px-4 py-2'> | |||
| <Button | |||
| className='w-full' | |||
| variant='tertiary' | |||
| onClick={() => handleAddCase()} | |||
| disabled={readOnly} | |||
| > | |||
| <div className='leading-[18px] text-xs font-normal text-gray-400'>{t(`${i18nPrefix}.elseDescription`)}</div> | |||
| </Field> | |||
| <RiAddLine className='mr-1 w-4 h-4' /> | |||
| ELIF | |||
| </Button> | |||
| </div> | |||
| <div className='my-2 mx-3 h-[1px] bg-divider-subtle'></div> | |||
| <Field | |||
| title={t(`${i18nPrefix}.else`)} | |||
| className='px-4 py-2' | |||
| > | |||
| <div className='leading-[18px] text-xs font-normal text-text-tertiary'>{t(`${i18nPrefix}.elseDescription`)}</div> | |||
| </Field> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Panel) | |||
| export default memo(Panel) | |||
| @@ -1,4 +1,10 @@ | |||
| import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types' | |||
| import type { VarType as NumberVarType } from '../tool/types' | |||
| import type { | |||
| CommonNodeType, | |||
| ValueSelector, | |||
| Var, | |||
| VarType, | |||
| } from '@/app/components/workflow/types' | |||
| export enum LogicalOperator { | |||
| and = 'and', | |||
| @@ -26,12 +32,26 @@ export enum ComparisonOperator { | |||
| export type Condition = { | |||
| id: string | |||
| varType: VarType | |||
| variable_selector: ValueSelector | |||
| comparison_operator?: ComparisonOperator | |||
| value: string | |||
| numberVarType?: NumberVarType | |||
| } | |||
| export type IfElseNodeType = CommonNodeType & { | |||
| export type CaseItem = { | |||
| case_id: string | |||
| logical_operator: LogicalOperator | |||
| conditions: Condition[] | |||
| } | |||
| export type IfElseNodeType = CommonNodeType & { | |||
| logical_operator?: LogicalOperator | |||
| conditions?: Condition[] | |||
| cases: CaseItem[] | |||
| } | |||
| 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 | |||
| @@ -1,76 +1,177 @@ | |||
| import { useCallback } from 'react' | |||
| import produce from 'immer' | |||
| import type { Var } from '../../types' | |||
| import { v4 as uuid4 } from 'uuid' | |||
| import type { | |||
| Var, | |||
| } from '../../types' | |||
| import { VarType } from '../../types' | |||
| import { getVarType } from '../_base/components/variable/utils' | |||
| import useNodeInfo from '../_base/hooks/use-node-info' | |||
| import { LogicalOperator } from './types' | |||
| import type { Condition, IfElseNodeType } from './types' | |||
| import type { | |||
| CaseItem, | |||
| HandleAddCondition, | |||
| HandleRemoveCondition, | |||
| HandleUpdateCondition, | |||
| HandleUpdateConditionLogicalOperator, | |||
| IfElseNodeType, | |||
| } from './types' | |||
| import { | |||
| branchNameCorrect, | |||
| getOperators, | |||
| } from './utils' | |||
| import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' | |||
| import { | |||
| useIsChatMode, | |||
| useEdgesInteractions, | |||
| useNodesReadOnly, | |||
| useWorkflow, | |||
| } from '@/app/components/workflow/hooks' | |||
| import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' | |||
| const useConfig = (id: string, payload: IfElseNodeType) => { | |||
| const { nodesReadOnly: readOnly } = useNodesReadOnly() | |||
| const { getBeforeNodesInSameBranch } = useWorkflow() | |||
| const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions() | |||
| const { inputs, setInputs } = useNodeCrud<IfElseNodeType>(id, payload) | |||
| const filterVar = useCallback((varPayload: Var) => { | |||
| return varPayload.type !== VarType.arrayFile | |||
| }, []) | |||
| const { | |||
| parentNode, | |||
| } = useNodeInfo(id) | |||
| const isChatMode = useIsChatMode() | |||
| const beforeNodes = getBeforeNodesInSameBranch(id) | |||
| availableVars, | |||
| availableNodesWithParent, | |||
| } = useAvailableVarList(id, { | |||
| onlyLeafNodeVar: false, | |||
| filterVar, | |||
| }) | |||
| const { inputs, setInputs } = useNodeCrud<IfElseNodeType>(id, payload) | |||
| const filterNumberVar = useCallback((varPayload: Var) => { | |||
| return varPayload.type === VarType.number | |||
| }, []) | |||
| const { | |||
| availableVars: availableNumberVars, | |||
| availableNodesWithParent: availableNumberNodesWithParent, | |||
| } = useAvailableVarList(id, { | |||
| onlyLeafNodeVar: false, | |||
| filterVar: filterNumberVar, | |||
| }) | |||
| const handleAddCase = useCallback(() => { | |||
| const newInputs = produce(inputs, () => { | |||
| if (inputs.cases) { | |||
| const case_id = uuid4() | |||
| inputs.cases.push({ | |||
| case_id, | |||
| logical_operator: LogicalOperator.and, | |||
| conditions: [], | |||
| }) | |||
| if (inputs._targetBranches) { | |||
| const elseCaseIndex = inputs._targetBranches.findIndex(branch => branch.id === 'false') | |||
| if (elseCaseIndex > -1) { | |||
| inputs._targetBranches = branchNameCorrect([ | |||
| ...inputs._targetBranches.slice(0, elseCaseIndex), | |||
| { | |||
| id: case_id, | |||
| name: '', | |||
| }, | |||
| ...inputs._targetBranches.slice(elseCaseIndex), | |||
| ]) | |||
| } | |||
| } | |||
| } | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| const handleRemoveCase = useCallback((caseId: string) => { | |||
| const newInputs = produce(inputs, (draft) => { | |||
| draft.cases = draft.cases?.filter(item => item.case_id !== caseId) | |||
| if (draft._targetBranches) | |||
| draft._targetBranches = branchNameCorrect(draft._targetBranches.filter(branch => branch.id !== caseId)) | |||
| handleEdgeDeleteByDeleteBranch(id, caseId) | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs, id, handleEdgeDeleteByDeleteBranch]) | |||
| const handleConditionsChange = useCallback((newConditions: Condition[]) => { | |||
| const handleSortCase = useCallback((newCases: (CaseItem & { id: string })[]) => { | |||
| const newInputs = produce(inputs, (draft) => { | |||
| draft.conditions = newConditions | |||
| draft.cases = newCases.filter(Boolean).map(item => ({ | |||
| id: item.id, | |||
| case_id: item.case_id, | |||
| logical_operator: item.logical_operator, | |||
| conditions: item.conditions, | |||
| })) | |||
| draft._targetBranches = branchNameCorrect([ | |||
| ...newCases.filter(Boolean).map(item => ({ id: item.case_id, name: '' })), | |||
| { id: 'false', name: '' }, | |||
| ]) | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| const handleAddCondition = useCallback(() => { | |||
| const handleAddCondition = useCallback<HandleAddCondition>((caseId, valueSelector, varItem) => { | |||
| const newInputs = produce(inputs, (draft) => { | |||
| draft.conditions.push({ | |||
| id: `${Date.now()}`, | |||
| variable_selector: [], | |||
| comparison_operator: undefined, | |||
| value: '', | |||
| }) | |||
| const targetCase = draft.cases?.find(item => item.case_id === caseId) | |||
| if (targetCase) { | |||
| targetCase.conditions.push({ | |||
| id: uuid4(), | |||
| varType: varItem.type, | |||
| variable_selector: valueSelector, | |||
| comparison_operator: getOperators(varItem.type)[0], | |||
| value: '', | |||
| }) | |||
| } | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| const handleLogicalOperatorToggle = useCallback(() => { | |||
| const handleRemoveCondition = useCallback<HandleRemoveCondition>((caseId, conditionId) => { | |||
| const newInputs = produce(inputs, (draft) => { | |||
| draft.logical_operator = draft.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and | |||
| const targetCase = draft.cases?.find(item => item.case_id === caseId) | |||
| if (targetCase) | |||
| targetCase.conditions = targetCase.conditions.filter(item => item.id !== conditionId) | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| const filterVar = useCallback((varPayload: Var) => { | |||
| return varPayload.type !== VarType.arrayFile | |||
| }, []) | |||
| const handleUpdateCondition = useCallback<HandleUpdateCondition>((caseId, conditionId, newCondition) => { | |||
| 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) | |||
| Object.assign(targetCondition, newCondition) | |||
| } | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| const varTypesList = (inputs.conditions || []).map((condition) => { | |||
| return getVarType({ | |||
| parentNode, | |||
| valueSelector: condition.variable_selector, | |||
| availableNodes: beforeNodes, | |||
| isChatMode, | |||
| const handleUpdateConditionLogicalOperator = useCallback<HandleUpdateConditionLogicalOperator>((caseId, value) => { | |||
| const newInputs = produce(inputs, (draft) => { | |||
| const targetCase = draft.cases?.find(item => item.case_id === caseId) | |||
| if (targetCase) | |||
| targetCase.logical_operator = value | |||
| }) | |||
| }) | |||
| setInputs(newInputs) | |||
| }, [inputs, setInputs]) | |||
| return { | |||
| readOnly, | |||
| inputs, | |||
| handleConditionsChange, | |||
| handleAddCondition, | |||
| handleLogicalOperatorToggle, | |||
| varTypesList, | |||
| filterVar, | |||
| filterNumberVar, | |||
| handleAddCase, | |||
| handleRemoveCase, | |||
| handleSortCase, | |||
| handleAddCondition, | |||
| handleRemoveCondition, | |||
| handleUpdateCondition, | |||
| handleUpdateConditionLogicalOperator, | |||
| nodesOutputVars: availableVars, | |||
| availableNodes: availableNodesWithParent, | |||
| nodesOutputNumberVars: availableNumberVars, | |||
| availableNumberNodes: availableNumberNodesWithParent, | |||
| } | |||
| } | |||
| @@ -1,4 +1,6 @@ | |||
| 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].includes(operator) | |||
| @@ -15,3 +17,80 @@ export const isComparisonOperatorNeedTranslate = (operator?: ComparisonOperator) | |||
| return false | |||
| return !notTranslateKey.includes(operator) | |||
| } | |||
| export const getOperators = (type?: VarType) => { | |||
| 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.arrayString: | |||
| case VarType.arrayNumber: | |||
| return [ | |||
| ComparisonOperator.contains, | |||
| ComparisonOperator.notContains, | |||
| ComparisonOperator.empty, | |||
| ComparisonOperator.notEmpty, | |||
| ] | |||
| case VarType.array: | |||
| case VarType.arrayObject: | |||
| return [ | |||
| 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].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}`, | |||
| } | |||
| }) | |||
| } | |||
| @@ -14,6 +14,7 @@ import type { | |||
| InputVar, | |||
| Node, | |||
| ToolWithProvider, | |||
| ValueSelector, | |||
| } from './types' | |||
| import { BlockEnum } from './types' | |||
| import { | |||
| @@ -23,6 +24,8 @@ import { | |||
| START_INITIAL_POSITION, | |||
| } from './constants' | |||
| import type { QuestionClassifierNodeType } from './nodes/question-classifier/types' | |||
| import type { IfElseNodeType } from './nodes/if-else/types' | |||
| import { branchNameCorrect } from './nodes/if-else/utils' | |||
| import type { ToolNodeType } from './nodes/tool/types' | |||
| import { CollectionType } from '@/app/components/tools/types' | |||
| import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' | |||
| @@ -114,16 +117,21 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { | |||
| node.data._connectedTargetHandleIds = connectedEdges.filter(edge => edge.target === node.id).map(edge => edge.targetHandle || 'target') | |||
| if (node.data.type === BlockEnum.IfElse) { | |||
| node.data._targetBranches = [ | |||
| { | |||
| id: 'true', | |||
| name: 'IS TRUE', | |||
| }, | |||
| { | |||
| id: 'false', | |||
| name: 'IS FALSE', | |||
| }, | |||
| ] | |||
| const nodeData = node.data as IfElseNodeType | |||
| if (!nodeData.cases && nodeData.logical_operator && nodeData.conditions) { | |||
| (node.data as IfElseNodeType).cases = [ | |||
| { | |||
| case_id: 'true', | |||
| logical_operator: nodeData.logical_operator, | |||
| conditions: nodeData.conditions, | |||
| }, | |||
| ] | |||
| } | |||
| node.data._targetBranches = branchNameCorrect([ | |||
| ...(node.data as IfElseNodeType).cases.map(item => ({ id: item.case_id, name: '' })), | |||
| { id: 'false', name: '' }, | |||
| ]) | |||
| } | |||
| if (node.data.type === BlockEnum.QuestionClassifier) { | |||
| @@ -184,6 +192,7 @@ export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => { | |||
| _connectedNodeIsSelected: edge.source === selectedNode.id || edge.target === selectedNode.id, | |||
| } as any | |||
| } | |||
| return edge | |||
| }) | |||
| } | |||
| @@ -463,3 +472,10 @@ export const isEventTargetInputArea = (target: HTMLElement) => { | |||
| if (target.contentEditable === 'true') | |||
| return true | |||
| } | |||
| export const variableTransformer = (v: ValueSelector | string) => { | |||
| if (typeof v === 'string') | |||
| return v.replace(/^{{#|#}}$/g, '').split('.') | |||
| return `{{#${v.join('.')}#}}` | |||
| } | |||
| @@ -364,6 +364,7 @@ const translation = { | |||
| enterValue: 'Enter value', | |||
| addCondition: 'Add Condition', | |||
| conditionNotSetup: 'Condition NOT setup', | |||
| selectVariable: 'Select variable...', | |||
| }, | |||
| variableAssigner: { | |||
| title: 'Assign variables', | |||
| @@ -364,6 +364,7 @@ const translation = { | |||
| enterValue: '输入值', | |||
| addCondition: '添加条件', | |||
| conditionNotSetup: '条件未设置', | |||
| selectVariable: '选择变量', | |||
| }, | |||
| variableAssigner: { | |||
| title: '变量赋值', | |||