feat: workflow continue on error (#11474)
parent
3b57b8c91f
commit
d1d76823d1
@ -0,0 +1,53 @@
|
|||||||
|
type CustomEdgeLinearGradientRenderProps = {
|
||||||
|
id: string
|
||||||
|
startColor: string
|
||||||
|
stopColor: string
|
||||||
|
position: {
|
||||||
|
x1: number
|
||||||
|
x2: number
|
||||||
|
y1: number
|
||||||
|
y2: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const CustomEdgeLinearGradientRender = ({
|
||||||
|
id,
|
||||||
|
startColor,
|
||||||
|
stopColor,
|
||||||
|
position,
|
||||||
|
}: CustomEdgeLinearGradientRenderProps) => {
|
||||||
|
const {
|
||||||
|
x1,
|
||||||
|
x2,
|
||||||
|
y1,
|
||||||
|
y2,
|
||||||
|
} = position
|
||||||
|
return (
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id={id}
|
||||||
|
gradientUnits='userSpaceOnUse'
|
||||||
|
x1={x1}
|
||||||
|
y1={y1}
|
||||||
|
x2={x2}
|
||||||
|
y2={y2}
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset='0%'
|
||||||
|
style={{
|
||||||
|
stopColor: startColor,
|
||||||
|
stopOpacity: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset='100%'
|
||||||
|
style={{
|
||||||
|
stopColor,
|
||||||
|
stopOpacity: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomEdgeLinearGradientRender
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import Collapse from '.'
|
||||||
|
|
||||||
|
type FieldCollapseProps = {
|
||||||
|
title: string
|
||||||
|
children: JSX.Element
|
||||||
|
}
|
||||||
|
const FieldCollapse = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: FieldCollapseProps) => {
|
||||||
|
return (
|
||||||
|
<div className='py-4'>
|
||||||
|
<Collapse
|
||||||
|
trigger={
|
||||||
|
<div className='flex items-center h-6 system-sm-semibold-uppercase text-text-secondary cursor-pointer'>{title}</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='px-4'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FieldCollapse
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { RiArrowDropRightLine } from '@remixicon/react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
export { default as FieldCollapse } from './field-collapse'
|
||||||
|
|
||||||
|
type CollapseProps = {
|
||||||
|
disabled?: boolean
|
||||||
|
trigger: JSX.Element
|
||||||
|
children: JSX.Element
|
||||||
|
collapsed?: boolean
|
||||||
|
onCollapse?: (collapsed: boolean) => void
|
||||||
|
}
|
||||||
|
const Collapse = ({
|
||||||
|
disabled,
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
collapsed,
|
||||||
|
onCollapse,
|
||||||
|
}: CollapseProps) => {
|
||||||
|
const [collapsedLocal, setCollapsedLocal] = useState(true)
|
||||||
|
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className='flex items-center'
|
||||||
|
onClick={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
setCollapsedLocal(!collapsedMerged)
|
||||||
|
onCollapse?.(!collapsedMerged)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='shrink-0 w-4 h-4'>
|
||||||
|
{
|
||||||
|
!disabled && (
|
||||||
|
<RiArrowDropRightLine
|
||||||
|
className={cn(
|
||||||
|
'w-4 h-4 text-text-tertiary',
|
||||||
|
!collapsedMerged && 'transform rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{trigger}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
!collapsedMerged && children
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Collapse
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import type { DefaultValueForm } from './types'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import { VarType } from '@/app/components/workflow/types'
|
||||||
|
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||||
|
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||||
|
|
||||||
|
type DefaultValueProps = {
|
||||||
|
forms: DefaultValueForm[]
|
||||||
|
onFormChange: (form: DefaultValueForm) => void
|
||||||
|
}
|
||||||
|
const DefaultValue = ({
|
||||||
|
forms,
|
||||||
|
onFormChange,
|
||||||
|
}: DefaultValueProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => {
|
||||||
|
return (payload: any) => {
|
||||||
|
let value
|
||||||
|
if (type === VarType.string || type === VarType.number)
|
||||||
|
value = payload.target.value
|
||||||
|
|
||||||
|
if (type === VarType.array || type === VarType.arrayNumber || type === VarType.arrayString || type === VarType.arrayObject || type === VarType.arrayFile || type === VarType.object)
|
||||||
|
value = payload
|
||||||
|
|
||||||
|
onFormChange({ key, type, value })
|
||||||
|
}
|
||||||
|
}, [onFormChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='px-4 pt-2'>
|
||||||
|
<div className='mb-2 body-xs-regular text-text-tertiary'>
|
||||||
|
{t('workflow.nodes.common.errorHandle.defaultValue.desc')}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href='https://docs.dify.ai/guides/workflow/error-handling'
|
||||||
|
target='_blank'
|
||||||
|
className='text-text-accent'
|
||||||
|
>
|
||||||
|
{t('workflow.common.learnMore')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-1'>
|
||||||
|
{
|
||||||
|
forms.map((form, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className='py-1'
|
||||||
|
>
|
||||||
|
<div className='flex items-center mb-1'>
|
||||||
|
<div className='mr-1 system-sm-medium text-text-primary'>{form.key}</div>
|
||||||
|
<div className='system-xs-regular text-text-tertiary'>{form.type}</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
(form.type === VarType.string || form.type === VarType.number) && (
|
||||||
|
<Input
|
||||||
|
type={form.type}
|
||||||
|
value={form.value || (form.type === VarType.string ? '' : 0)}
|
||||||
|
onChange={getFormChangeHandler({ key: form.key, type: form.type })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(
|
||||||
|
form.type === VarType.array
|
||||||
|
|| form.type === VarType.arrayNumber
|
||||||
|
|| form.type === VarType.arrayString
|
||||||
|
|| form.type === VarType.arrayObject
|
||||||
|
|| form.type === VarType.object
|
||||||
|
) && (
|
||||||
|
<CodeEditor
|
||||||
|
language={CodeLanguage.json}
|
||||||
|
value={form.value}
|
||||||
|
onChange={getFormChangeHandler({ key: form.key, type: form.type })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DefaultValue
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useUpdateNodeInternals } from 'reactflow'
|
||||||
|
import { NodeSourceHandle } from '../node-handle'
|
||||||
|
import { ErrorHandleTypeEnum } from './types'
|
||||||
|
import type { Node } from '@/app/components/workflow/types'
|
||||||
|
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type ErrorHandleOnNodeProps = Pick<Node, 'id' | 'data'>
|
||||||
|
const ErrorHandleOnNode = ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
}: ErrorHandleOnNodeProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { error_strategy } = data
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error_strategy === ErrorHandleTypeEnum.failBranch)
|
||||||
|
updateNodeInternals(id)
|
||||||
|
}, [error_strategy, id, updateNodeInternals])
|
||||||
|
|
||||||
|
if (!error_strategy)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative pt-1 pb-2 px-3'>
|
||||||
|
<div className={cn(
|
||||||
|
'relative flex items-center justify-between px-[5px] h-6 bg-workflow-block-parma-bg rounded-md',
|
||||||
|
data._runningStatus === NodeRunningStatus.Exception && 'border-[0.5px] border-components-badge-status-light-warning-halo bg-state-warning-hover',
|
||||||
|
)}>
|
||||||
|
<div className='system-xs-medium-uppercase text-text-tertiary'>
|
||||||
|
{t('workflow.common.onFailure')}
|
||||||
|
</div>
|
||||||
|
<div className={cn(
|
||||||
|
'system-xs-medium text-text-secondary',
|
||||||
|
data._runningStatus === NodeRunningStatus.Exception && 'text-text-warning',
|
||||||
|
)}>
|
||||||
|
{
|
||||||
|
error_strategy === ErrorHandleTypeEnum.defaultValue && (
|
||||||
|
t('workflow.nodes.common.errorHandle.defaultValue.output')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
error_strategy === ErrorHandleTypeEnum.failBranch && (
|
||||||
|
t('workflow.nodes.common.errorHandle.failBranch.title')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
error_strategy === ErrorHandleTypeEnum.failBranch && (
|
||||||
|
<NodeSourceHandle
|
||||||
|
id={id}
|
||||||
|
data={data}
|
||||||
|
handleId={ErrorHandleTypeEnum.failBranch}
|
||||||
|
handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2 after:!bg-workflow-link-line-failure-button-bg'
|
||||||
|
nodeSelectorClassName='!bg-workflow-link-line-failure-button-bg'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorHandleOnNode
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Collapse from '../collapse'
|
||||||
|
import { ErrorHandleTypeEnum } from './types'
|
||||||
|
import ErrorHandleTypeSelector from './error-handle-type-selector'
|
||||||
|
import FailBranchCard from './fail-branch-card'
|
||||||
|
import DefaultValue from './default-value'
|
||||||
|
import {
|
||||||
|
useDefaultValue,
|
||||||
|
useErrorHandle,
|
||||||
|
} from './hooks'
|
||||||
|
import type { DefaultValueForm } from './types'
|
||||||
|
import type {
|
||||||
|
CommonNodeType,
|
||||||
|
Node,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||||
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
|
||||||
|
type ErrorHandleProps = Pick<Node, 'id' | 'data'>
|
||||||
|
const ErrorHandle = ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
}: ErrorHandleProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { error_strategy, default_value } = data
|
||||||
|
const {
|
||||||
|
collapsed,
|
||||||
|
setCollapsed,
|
||||||
|
handleErrorHandleTypeChange,
|
||||||
|
} = useErrorHandle(id, data)
|
||||||
|
const { handleFormChange } = useDefaultValue(id)
|
||||||
|
|
||||||
|
const getHandleErrorHandleTypeChange = useCallback((data: CommonNodeType) => {
|
||||||
|
return (value: ErrorHandleTypeEnum) => {
|
||||||
|
handleErrorHandleTypeChange(value, data)
|
||||||
|
}
|
||||||
|
}, [handleErrorHandleTypeChange])
|
||||||
|
|
||||||
|
const getHandleFormChange = useCallback((data: CommonNodeType) => {
|
||||||
|
return (v: DefaultValueForm) => {
|
||||||
|
handleFormChange(v, data)
|
||||||
|
}
|
||||||
|
}, [handleFormChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Split />
|
||||||
|
<div className='py-4'>
|
||||||
|
<Collapse
|
||||||
|
disabled={!error_strategy}
|
||||||
|
collapsed={collapsed}
|
||||||
|
onCollapse={setCollapsed}
|
||||||
|
trigger={
|
||||||
|
<div className='grow flex items-center justify-between pr-4'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>
|
||||||
|
{t('workflow.nodes.common.errorHandle.title')}
|
||||||
|
</div>
|
||||||
|
<Tooltip popupContent={t('workflow.nodes.common.errorHandle.tip')} />
|
||||||
|
</div>
|
||||||
|
<ErrorHandleTypeSelector
|
||||||
|
value={error_strategy || ErrorHandleTypeEnum.none}
|
||||||
|
onSelected={getHandleErrorHandleTypeChange(data)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
error_strategy === ErrorHandleTypeEnum.failBranch && !collapsed && (
|
||||||
|
<FailBranchCard />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
error_strategy === ErrorHandleTypeEnum.defaultValue && !collapsed && !!default_value?.length && (
|
||||||
|
<DefaultValue
|
||||||
|
forms={default_value}
|
||||||
|
onFormChange={getHandleFormChange(data)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorHandle
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiAlertFill } from '@remixicon/react'
|
||||||
|
import { ErrorHandleTypeEnum } from './types'
|
||||||
|
|
||||||
|
type ErrorHandleTipProps = {
|
||||||
|
type?: ErrorHandleTypeEnum
|
||||||
|
}
|
||||||
|
const ErrorHandleTip = ({
|
||||||
|
type,
|
||||||
|
}: ErrorHandleTipProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const text = useMemo(() => {
|
||||||
|
if (type === ErrorHandleTypeEnum.failBranch)
|
||||||
|
return t('workflow.nodes.common.errorHandle.failBranch.inLog')
|
||||||
|
|
||||||
|
if (type === ErrorHandleTypeEnum.defaultValue)
|
||||||
|
return t('workflow.nodes.common.errorHandle.defaultValue.inLog')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!type)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='relative flex p-2 pr-[52px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xs'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='absolute inset-0 opacity-40 rounded-lg'
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<RiAlertFill className='shrink-0 mr-1 w-4 h-4 text-text-warning-secondary' />
|
||||||
|
<div className='grow system-xs-medium text-text-primary'>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorHandleTip
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
RiCheckLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { ErrorHandleTypeEnum } from './types'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
|
||||||
|
type ErrorHandleTypeSelectorProps = {
|
||||||
|
value: ErrorHandleTypeEnum
|
||||||
|
onSelected: (value: ErrorHandleTypeEnum) => void
|
||||||
|
}
|
||||||
|
const ErrorHandleTypeSelector = ({
|
||||||
|
value,
|
||||||
|
onSelected,
|
||||||
|
}: ErrorHandleTypeSelectorProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
value: ErrorHandleTypeEnum.none,
|
||||||
|
label: t('workflow.nodes.common.errorHandle.none.title'),
|
||||||
|
description: t('workflow.nodes.common.errorHandle.none.desc'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ErrorHandleTypeEnum.defaultValue,
|
||||||
|
label: t('workflow.nodes.common.errorHandle.defaultValue.title'),
|
||||||
|
description: t('workflow.nodes.common.errorHandle.defaultValue.desc'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ErrorHandleTypeEnum.failBranch,
|
||||||
|
label: t('workflow.nodes.common.errorHandle.failBranch.title'),
|
||||||
|
description: t('workflow.nodes.common.errorHandle.failBranch.desc'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const selectedOption = options.find(option => option.value === value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
placement='bottom-end'
|
||||||
|
offset={4}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setOpen(v => !v)
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
{selectedOption?.label}
|
||||||
|
<RiArrowDownSLine className='w-3.5 h-3.5' />
|
||||||
|
</Button>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-[11]'>
|
||||||
|
<div className='p-1 w-[280px] border-[0.5px] border-components-panel-border rounded-xl bg-components-panel-bg-blur shadow-lg'>
|
||||||
|
{
|
||||||
|
options.map(option => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className='flex p-2 pr-3 rounded-lg hover:bg-state-base-hover cursor-pointer'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onSelected(option.value)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mr-1 w-4 shrink-0'>
|
||||||
|
{
|
||||||
|
value === option.value && (
|
||||||
|
<RiCheckLine className='w-4 h-4 text-text-accent' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className='grow'>
|
||||||
|
<div className='mb-0.5 system-sm-semibold text-text-secondary'>{option.label}</div>
|
||||||
|
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorHandleTypeSelector
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { RiMindMap } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const FailBranchCard = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='pt-2 px-4'>
|
||||||
|
<div className='p-4 rounded-[10px] bg-workflow-process-bg'>
|
||||||
|
<div className='flex items-center justify-center mb-2 w-8 h-8 rounded-[10px] border-[0.5px] bg-components-card-bg shadow-lg'>
|
||||||
|
<RiMindMap className='w-5 h-5 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
<div className='mb-1 system-sm-medium text-text-secondary'>
|
||||||
|
{t('workflow.nodes.common.errorHandle.failBranch.customize')}
|
||||||
|
</div>
|
||||||
|
<div className='system-xs-regular text-text-tertiary'>
|
||||||
|
{t('workflow.nodes.common.errorHandle.failBranch.customizeTip')}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href='https://docs.dify.ai/guides/workflow/error-handling'
|
||||||
|
target='_blank'
|
||||||
|
className='text-text-accent'
|
||||||
|
>
|
||||||
|
{t('workflow.common.learnMore')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FailBranchCard
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { ErrorHandleTypeEnum } from './types'
|
||||||
|
import type { DefaultValueForm } from './types'
|
||||||
|
import { getDefaultValue } from './utils'
|
||||||
|
import type {
|
||||||
|
CommonNodeType,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
import {
|
||||||
|
useEdgesInteractions,
|
||||||
|
useNodeDataUpdate,
|
||||||
|
} from '@/app/components/workflow/hooks'
|
||||||
|
|
||||||
|
export const useDefaultValue = (
|
||||||
|
id: string,
|
||||||
|
) => {
|
||||||
|
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||||
|
const handleFormChange = useCallback((
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
type,
|
||||||
|
}: DefaultValueForm,
|
||||||
|
data: CommonNodeType,
|
||||||
|
) => {
|
||||||
|
const default_value = data.default_value || []
|
||||||
|
const index = default_value.findIndex(form => form.key === key)
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
const newDefaultValue = [...default_value]
|
||||||
|
newDefaultValue[index].value = value
|
||||||
|
handleNodeDataUpdateWithSyncDraft({
|
||||||
|
id,
|
||||||
|
data: {
|
||||||
|
default_value: newDefaultValue,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNodeDataUpdateWithSyncDraft({
|
||||||
|
id,
|
||||||
|
data: {
|
||||||
|
default_value: [
|
||||||
|
...default_value,
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleFormChange,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useErrorHandle = (
|
||||||
|
id: string,
|
||||||
|
data: CommonNodeType,
|
||||||
|
) => {
|
||||||
|
const initCollapsed = useMemo(() => {
|
||||||
|
if (data.error_strategy === ErrorHandleTypeEnum.none)
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}, [data.error_strategy])
|
||||||
|
const [collapsed, setCollapsed] = useState(initCollapsed)
|
||||||
|
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||||
|
const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
|
||||||
|
|
||||||
|
const handleErrorHandleTypeChange = useCallback((value: ErrorHandleTypeEnum, data: CommonNodeType) => {
|
||||||
|
if (data.error_strategy === value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (value === ErrorHandleTypeEnum.none) {
|
||||||
|
handleNodeDataUpdateWithSyncDraft({
|
||||||
|
id,
|
||||||
|
data: {
|
||||||
|
error_strategy: undefined,
|
||||||
|
default_value: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setCollapsed(true)
|
||||||
|
handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === ErrorHandleTypeEnum.failBranch) {
|
||||||
|
handleNodeDataUpdateWithSyncDraft({
|
||||||
|
id,
|
||||||
|
data: {
|
||||||
|
error_strategy: value,
|
||||||
|
default_value: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setCollapsed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === ErrorHandleTypeEnum.defaultValue) {
|
||||||
|
handleNodeDataUpdateWithSyncDraft({
|
||||||
|
id,
|
||||||
|
data: {
|
||||||
|
error_strategy: value,
|
||||||
|
default_value: getDefaultValue(data),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setCollapsed(false)
|
||||||
|
handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch)
|
||||||
|
}
|
||||||
|
}, [id, handleNodeDataUpdateWithSyncDraft, handleEdgeDeleteByDeleteBranch])
|
||||||
|
|
||||||
|
return {
|
||||||
|
collapsed,
|
||||||
|
setCollapsed,
|
||||||
|
handleErrorHandleTypeChange,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import type { VarType } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
export enum ErrorHandleTypeEnum {
|
||||||
|
none = 'none',
|
||||||
|
failBranch = 'fail-branch',
|
||||||
|
defaultValue = 'default-value',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefaultValueForm = {
|
||||||
|
key: string
|
||||||
|
type: VarType
|
||||||
|
value?: any
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||||
|
import {
|
||||||
|
BlockEnum,
|
||||||
|
VarType,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
|
||||||
|
|
||||||
|
const getDefaultValueByType = (type: VarType) => {
|
||||||
|
if (type === VarType.string)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
if (type === VarType.number)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if (type === VarType.object)
|
||||||
|
return '{}'
|
||||||
|
|
||||||
|
if (type === VarType.arrayObject || type === VarType.arrayString || type === VarType.arrayNumber || type === VarType.arrayFile)
|
||||||
|
return '[]'
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDefaultValue = (data: CommonNodeType) => {
|
||||||
|
const { type } = data
|
||||||
|
|
||||||
|
if (type === BlockEnum.LLM) {
|
||||||
|
return [{
|
||||||
|
key: 'text',
|
||||||
|
type: VarType.string,
|
||||||
|
value: getDefaultValueByType(VarType.string),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === BlockEnum.HttpRequest) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'body',
|
||||||
|
type: VarType.string,
|
||||||
|
value: getDefaultValueByType(VarType.string),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status_code',
|
||||||
|
type: VarType.number,
|
||||||
|
value: getDefaultValueByType(VarType.number),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'headers',
|
||||||
|
type: VarType.object,
|
||||||
|
value: getDefaultValueByType(VarType.object),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === BlockEnum.Tool) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'text',
|
||||||
|
type: VarType.string,
|
||||||
|
value: getDefaultValueByType(VarType.string),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'json',
|
||||||
|
type: VarType.arrayObject,
|
||||||
|
value: getDefaultValueByType(VarType.arrayObject),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === BlockEnum.Code) {
|
||||||
|
const { outputs } = data as CodeNodeType
|
||||||
|
|
||||||
|
return Object.keys(outputs).map((key) => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
type: outputs[key].type,
|
||||||
|
value: getDefaultValueByType(outputs[key].type),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue