mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
test(workflow): add helper specs and raise targeted workflow coverage (#33995)
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,172 @@
|
||||
import type { IfElseNodeType } from '../types'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import { LogicalOperator } from '../types'
|
||||
import {
|
||||
addCase,
|
||||
addCondition,
|
||||
addSubVariableCondition,
|
||||
filterAllVars,
|
||||
filterNumberVars,
|
||||
getVarsIsVarFileAttribute,
|
||||
removeCase,
|
||||
removeCondition,
|
||||
removeSubVariableCondition,
|
||||
sortCases,
|
||||
toggleConditionLogicalOperator,
|
||||
toggleSubVariableConditionLogicalOperator,
|
||||
updateCondition,
|
||||
updateSubVariableCondition,
|
||||
} from '../use-config.helpers'
|
||||
|
||||
type TestIfElseInputs = ReturnType<typeof createInputs>
|
||||
|
||||
const createInputs = (): IfElseNodeType => ({
|
||||
title: 'If/Else',
|
||||
desc: '',
|
||||
type: BlockEnum.IfElse,
|
||||
cases: [{
|
||||
case_id: 'case-1',
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [{
|
||||
id: 'condition-1',
|
||||
varType: VarType.string,
|
||||
variable_selector: ['node', 'value'],
|
||||
comparison_operator: 'contains',
|
||||
value: '',
|
||||
}],
|
||||
}],
|
||||
_targetBranches: [
|
||||
{ id: 'case-1', name: 'Case 1' },
|
||||
{ id: 'false', name: 'Else' },
|
||||
],
|
||||
} as unknown as IfElseNodeType)
|
||||
|
||||
describe('if-else use-config helpers', () => {
|
||||
it('filters vars and derives file attribute flags', () => {
|
||||
expect(filterAllVars()).toBe(true)
|
||||
expect(filterNumberVars({ type: VarType.number } as never)).toBe(true)
|
||||
expect(filterNumberVars({ type: VarType.string } as never)).toBe(false)
|
||||
expect(getVarsIsVarFileAttribute(createInputs().cases, selector => selector[1] === 'value')).toEqual({
|
||||
'condition-1': true,
|
||||
})
|
||||
})
|
||||
|
||||
it('adds, removes and sorts cases while keeping target branches aligned', () => {
|
||||
const added = addCase(createInputs())
|
||||
expect(added.cases).toHaveLength(2)
|
||||
expect(added._targetBranches?.map(branch => branch.id)).toContain('false')
|
||||
|
||||
const removed = removeCase(added, 'case-1')
|
||||
expect(removed.cases?.some(item => item.case_id === 'case-1')).toBe(false)
|
||||
|
||||
const sorted = sortCases(createInputs(), [
|
||||
{ id: 'display-2', case_id: 'case-2', logical_operator: LogicalOperator.or, conditions: [] },
|
||||
{ id: 'display-1', case_id: 'case-1', logical_operator: LogicalOperator.and, conditions: [] },
|
||||
] as unknown as Parameters<typeof sortCases>[1])
|
||||
expect(sorted.cases?.map(item => item.case_id)).toEqual(['case-2', 'case-1'])
|
||||
expect(sorted._targetBranches?.map(branch => branch.id)).toEqual(['case-2', 'case-1', 'false'])
|
||||
})
|
||||
|
||||
it('adds, updates, toggles and removes conditions and sub-conditions', () => {
|
||||
const withCondition = addCondition({
|
||||
inputs: createInputs(),
|
||||
caseId: 'case-1',
|
||||
valueSelector: ['node', 'flag'],
|
||||
variable: { type: VarType.boolean } as never,
|
||||
isVarFileAttribute: false,
|
||||
})
|
||||
expect(withCondition.cases?.[0]?.conditions).toHaveLength(2)
|
||||
expect(withCondition.cases?.[0]?.conditions[1]).toEqual(expect.objectContaining({
|
||||
value: false,
|
||||
variable_selector: ['node', 'flag'],
|
||||
}))
|
||||
|
||||
const updatedCondition = updateCondition(withCondition, 'case-1', 'condition-1', {
|
||||
id: 'condition-1',
|
||||
value: 'next',
|
||||
comparison_operator: '=',
|
||||
} as Parameters<typeof updateCondition>[3])
|
||||
expect(updatedCondition.cases?.[0]?.conditions[0]).toEqual(expect.objectContaining({
|
||||
value: 'next',
|
||||
comparison_operator: '=',
|
||||
}))
|
||||
|
||||
const toggled = toggleConditionLogicalOperator(updatedCondition, 'case-1')
|
||||
expect(toggled.cases?.[0]?.logical_operator).toBe(LogicalOperator.or)
|
||||
|
||||
const withSubCondition = addSubVariableCondition(toggled, 'case-1', 'condition-1', 'name')
|
||||
expect(withSubCondition.cases?.[0]?.conditions[0]?.sub_variable_condition?.conditions[0]).toEqual(expect.objectContaining({
|
||||
key: 'name',
|
||||
value: '',
|
||||
}))
|
||||
|
||||
const firstSubConditionId = withSubCondition.cases?.[0]?.conditions[0]?.sub_variable_condition?.conditions[0]?.id
|
||||
expect(firstSubConditionId).toBeTruthy()
|
||||
const updatedSubCondition = updateSubVariableCondition(
|
||||
withSubCondition,
|
||||
'case-1',
|
||||
'condition-1',
|
||||
firstSubConditionId!,
|
||||
{ key: 'size', comparison_operator: '>', value: '10' } as TestIfElseInputs['cases'][number]['conditions'][number],
|
||||
)
|
||||
expect(updatedSubCondition.cases?.[0]?.conditions[0]?.sub_variable_condition?.conditions[0]).toEqual(expect.objectContaining({
|
||||
key: 'size',
|
||||
value: '10',
|
||||
}))
|
||||
|
||||
const toggledSub = toggleSubVariableConditionLogicalOperator(updatedSubCondition, 'case-1', 'condition-1')
|
||||
expect(toggledSub.cases?.[0]?.conditions[0]?.sub_variable_condition?.logical_operator).toBe(LogicalOperator.or)
|
||||
|
||||
const removedSub = removeSubVariableCondition(
|
||||
toggledSub,
|
||||
'case-1',
|
||||
'condition-1',
|
||||
firstSubConditionId!,
|
||||
)
|
||||
expect(removedSub.cases?.[0]?.conditions[0]?.sub_variable_condition?.conditions).toEqual([])
|
||||
|
||||
const removedCondition = removeCondition(removedSub, 'case-1', 'condition-1')
|
||||
expect(removedCondition.cases?.[0]?.conditions.some(item => item.id === 'condition-1')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps inputs unchanged when guard branches short-circuit helper updates', () => {
|
||||
const unchangedWithoutCases = addCase({
|
||||
...createInputs(),
|
||||
cases: undefined,
|
||||
} as unknown as IfElseNodeType)
|
||||
expect(unchangedWithoutCases.cases).toBeUndefined()
|
||||
|
||||
const withoutTargetBranches = addCase({
|
||||
...createInputs(),
|
||||
_targetBranches: undefined,
|
||||
})
|
||||
expect(withoutTargetBranches._targetBranches).toBeUndefined()
|
||||
|
||||
const withoutElseBranch = addCase({
|
||||
...createInputs(),
|
||||
_targetBranches: [{ id: 'case-1', name: 'Case 1' }],
|
||||
})
|
||||
expect(withoutElseBranch._targetBranches).toEqual([{ id: 'case-1', name: 'Case 1' }])
|
||||
|
||||
const unchangedWhenConditionMissing = addSubVariableCondition(createInputs(), 'case-1', 'missing-condition', 'name')
|
||||
expect(unchangedWhenConditionMissing).toEqual(createInputs())
|
||||
|
||||
const unchangedWhenSubConditionMissing = removeSubVariableCondition(createInputs(), 'case-1', 'condition-1', 'missing-sub')
|
||||
expect(unchangedWhenSubConditionMissing).toEqual(createInputs())
|
||||
|
||||
const unchangedWhenCaseIsMissingForCondition = addCondition({
|
||||
inputs: createInputs(),
|
||||
caseId: 'missing-case',
|
||||
valueSelector: ['node', 'value'],
|
||||
variable: { type: VarType.string } as never,
|
||||
isVarFileAttribute: false,
|
||||
})
|
||||
expect(unchangedWhenCaseIsMissingForCondition).toEqual(createInputs())
|
||||
|
||||
const unchangedWhenCaseMissing = toggleConditionLogicalOperator(createInputs(), 'missing-case')
|
||||
expect(unchangedWhenCaseMissing).toEqual(createInputs())
|
||||
|
||||
const unchangedWhenSubVariableGroupMissing = toggleSubVariableConditionLogicalOperator(createInputs(), 'case-1', 'condition-1')
|
||||
expect(unchangedWhenSubVariableGroupMissing).toEqual(createInputs())
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,266 @@
|
||||
import type { IfElseNodeType } from '../types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
createNodeCrudModuleMock,
|
||||
createUuidModuleMock,
|
||||
} from '../../__tests__/use-config-test-utils'
|
||||
import { ComparisonOperator, LogicalOperator } from '../types'
|
||||
import useConfig from '../use-config'
|
||||
|
||||
const mockSetInputs = vi.hoisted(() => vi.fn())
|
||||
const mockHandleEdgeDeleteByDeleteBranch = vi.hoisted(() => vi.fn())
|
||||
const mockUpdateNodeInternals = vi.hoisted(() => vi.fn())
|
||||
const mockGetIsVarFileAttribute = vi.hoisted(() => vi.fn())
|
||||
const mockUuid = vi.hoisted(() => vi.fn(() => 'generated-id'))
|
||||
|
||||
vi.mock('uuid', () => ({
|
||||
...createUuidModuleMock(mockUuid),
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', async () => {
|
||||
const actual = await vi.importActual<typeof import('reactflow')>('reactflow')
|
||||
return {
|
||||
...actual,
|
||||
useUpdateNodeInternals: () => mockUpdateNodeInternals,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useNodesReadOnly: () => ({ nodesReadOnly: false }),
|
||||
useEdgesInteractions: () => ({
|
||||
handleEdgeDeleteByDeleteBranch: (...args: unknown[]) => mockHandleEdgeDeleteByDeleteBranch(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
|
||||
...createNodeCrudModuleMock<IfElseNodeType>(mockSetInputs),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-available-var-list', () => ({
|
||||
__esModule: true,
|
||||
default: (_id: string, { filterVar }: { filterVar: (value: { type: VarType }) => boolean }) => ({
|
||||
availableVars: filterVar({ type: VarType.number })
|
||||
? [{ nodeId: 'node-1', title: 'Start', vars: [{ variable: 'score', type: VarType.number }] }]
|
||||
: [{ nodeId: 'node-1', title: 'Start', vars: [{ variable: 'answer', type: VarType.string }] }],
|
||||
availableNodesWithParent: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../use-is-var-file-attribute', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
getIsVarFileAttribute: (...args: unknown[]) => mockGetIsVarFileAttribute(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
const createPayload = (overrides: Partial<IfElseNodeType> = {}): IfElseNodeType => ({
|
||||
title: 'If Else',
|
||||
desc: '',
|
||||
type: BlockEnum.IfElse,
|
||||
isInIteration: false,
|
||||
isInLoop: false,
|
||||
cases: [{
|
||||
case_id: 'case-1',
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [{
|
||||
id: 'condition-1',
|
||||
varType: VarType.string,
|
||||
variable_selector: ['node-1', 'answer'],
|
||||
comparison_operator: ComparisonOperator.contains,
|
||||
value: 'hello',
|
||||
}],
|
||||
}],
|
||||
_targetBranches: [
|
||||
{ id: 'case-1', name: 'IF' },
|
||||
{ id: 'false', name: 'ELSE' },
|
||||
],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('useConfig', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetIsVarFileAttribute.mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('should expose derived vars and file-attribute flags', () => {
|
||||
const { result } = renderHook(() => useConfig('if-node', createPayload()))
|
||||
|
||||
expect(result.current.readOnly).toBe(false)
|
||||
expect(result.current.filterVar()).toBe(true)
|
||||
expect(result.current.filterNumberVar({ type: VarType.number } as never)).toBe(true)
|
||||
expect(result.current.filterNumberVar({ type: VarType.string } as never)).toBe(false)
|
||||
expect(result.current.nodesOutputVars).toHaveLength(1)
|
||||
expect(result.current.nodesOutputNumberVars).toHaveLength(1)
|
||||
expect(result.current.varsIsVarFileAttribute).toEqual({ 'condition-1': false })
|
||||
})
|
||||
|
||||
it('should manage cases and conditions', () => {
|
||||
const { result } = renderHook(() => useConfig('if-node', createPayload()))
|
||||
|
||||
result.current.handleAddCase()
|
||||
result.current.handleRemoveCase('generated-id')
|
||||
result.current.handleAddCondition('case-1', ['node-1', 'score'], { type: VarType.number } as never)
|
||||
result.current.handleUpdateCondition('case-1', 'condition-1', {
|
||||
id: 'condition-1',
|
||||
varType: VarType.number,
|
||||
variable_selector: ['node-1', 'score'],
|
||||
comparison_operator: ComparisonOperator.largerThan,
|
||||
value: '3',
|
||||
})
|
||||
result.current.handleRemoveCondition('case-1', 'condition-1')
|
||||
result.current.handleToggleConditionLogicalOperator('case-1')
|
||||
result.current.handleSortCase([{
|
||||
id: 'sortable-1',
|
||||
case_id: 'case-1',
|
||||
logical_operator: LogicalOperator.or,
|
||||
conditions: [],
|
||||
}])
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cases: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
case_id: 'generated-id',
|
||||
logical_operator: LogicalOperator.and,
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cases: [
|
||||
expect.objectContaining({
|
||||
case_id: 'case-1',
|
||||
logical_operator: LogicalOperator.or,
|
||||
}),
|
||||
],
|
||||
_targetBranches: [
|
||||
{ id: 'case-1', name: 'IF' },
|
||||
{ id: 'false', name: 'ELSE' },
|
||||
],
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cases: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'generated-id',
|
||||
variable_selector: ['node-1', 'score'],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cases: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'condition-1',
|
||||
comparison_operator: ComparisonOperator.largerThan,
|
||||
value: '3',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cases: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
logical_operator: LogicalOperator.or,
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockHandleEdgeDeleteByDeleteBranch).toHaveBeenCalledWith('if-node', 'generated-id')
|
||||
expect(mockUpdateNodeInternals).toHaveBeenCalledWith('if-node')
|
||||
})
|
||||
|
||||
it('should manage sub-variable conditions', () => {
|
||||
const payload = createPayload({
|
||||
cases: [{
|
||||
case_id: 'case-1',
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [{
|
||||
id: 'condition-1',
|
||||
varType: VarType.file,
|
||||
variable_selector: ['node-1', 'files'],
|
||||
comparison_operator: ComparisonOperator.exists,
|
||||
value: '',
|
||||
sub_variable_condition: {
|
||||
case_id: 'sub-case-1',
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [{
|
||||
id: 'sub-1',
|
||||
key: 'name',
|
||||
varType: VarType.string,
|
||||
comparison_operator: ComparisonOperator.contains,
|
||||
value: '',
|
||||
}],
|
||||
},
|
||||
}],
|
||||
}],
|
||||
})
|
||||
const { result } = renderHook(() => useConfig('if-node', payload))
|
||||
|
||||
result.current.handleAddSubVariableCondition('case-1', 'condition-1', 'name')
|
||||
result.current.handleUpdateSubVariableCondition('case-1', 'condition-1', 'sub-1', {
|
||||
id: 'sub-1',
|
||||
key: 'size',
|
||||
varType: VarType.string,
|
||||
comparison_operator: ComparisonOperator.is,
|
||||
value: '2',
|
||||
})
|
||||
result.current.handleRemoveSubVariableCondition('case-1', 'condition-1', 'sub-1')
|
||||
result.current.handleToggleSubVariableConditionLogicalOperator('case-1', 'condition-1')
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cases: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
sub_variable_condition: expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'generated-id',
|
||||
key: 'name',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cases: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
sub_variable_condition: expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'sub-1',
|
||||
key: 'size',
|
||||
value: '2',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cases: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
sub_variable_condition: expect.objectContaining({
|
||||
logical_operator: LogicalOperator.or,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
})
|
||||
})
|
||||
237
web/app/components/workflow/nodes/if-else/use-config.helpers.ts
Normal file
237
web/app/components/workflow/nodes/if-else/use-config.helpers.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import type { Branch, Var } from '../../types'
|
||||
import type { CaseItem, Condition, IfElseNodeType } from './types'
|
||||
import { produce } from 'immer'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { VarType } from '../../types'
|
||||
import { LogicalOperator } from './types'
|
||||
import {
|
||||
branchNameCorrect,
|
||||
getOperators,
|
||||
} from './utils'
|
||||
|
||||
export const filterAllVars = () => true
|
||||
|
||||
export const filterNumberVars = (varPayload: Var) => varPayload.type === VarType.number
|
||||
|
||||
export const getVarsIsVarFileAttribute = (
|
||||
cases: IfElseNodeType['cases'],
|
||||
getIsVarFileAttribute: (valueSelector: string[]) => boolean,
|
||||
) => {
|
||||
const conditions: Record<string, boolean> = {}
|
||||
cases?.forEach((caseItem) => {
|
||||
caseItem.conditions.forEach((condition) => {
|
||||
if (condition.variable_selector)
|
||||
conditions[condition.id] = getIsVarFileAttribute(condition.variable_selector)
|
||||
})
|
||||
})
|
||||
return conditions
|
||||
}
|
||||
|
||||
const getTargetBranchesWithNewCase = (targetBranches: Branch[] | undefined, caseId: string) => {
|
||||
if (!targetBranches)
|
||||
return targetBranches
|
||||
|
||||
const elseCaseIndex = targetBranches.findIndex(branch => branch.id === 'false')
|
||||
if (elseCaseIndex < 0)
|
||||
return targetBranches
|
||||
|
||||
return branchNameCorrect([
|
||||
...targetBranches.slice(0, elseCaseIndex),
|
||||
{
|
||||
id: caseId,
|
||||
name: '',
|
||||
},
|
||||
...targetBranches.slice(elseCaseIndex),
|
||||
])
|
||||
}
|
||||
|
||||
export const addCase = (inputs: IfElseNodeType) => produce(inputs, (draft) => {
|
||||
if (!draft.cases)
|
||||
return
|
||||
|
||||
const caseId = uuid4()
|
||||
draft.cases.push({
|
||||
case_id: caseId,
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
})
|
||||
draft._targetBranches = getTargetBranchesWithNewCase(draft._targetBranches, caseId)
|
||||
})
|
||||
|
||||
export const removeCase = (
|
||||
inputs: IfElseNodeType,
|
||||
caseId: string,
|
||||
) => 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))
|
||||
})
|
||||
|
||||
export const sortCases = (
|
||||
inputs: IfElseNodeType,
|
||||
newCases: (CaseItem & { id: string })[],
|
||||
) => produce(inputs, (draft) => {
|
||||
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: '' },
|
||||
])
|
||||
})
|
||||
|
||||
export const addCondition = ({
|
||||
inputs,
|
||||
caseId,
|
||||
valueSelector,
|
||||
variable,
|
||||
isVarFileAttribute,
|
||||
}: {
|
||||
inputs: IfElseNodeType
|
||||
caseId: string
|
||||
valueSelector: string[]
|
||||
variable: Var
|
||||
isVarFileAttribute: boolean
|
||||
}) => produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (!targetCase)
|
||||
return
|
||||
|
||||
targetCase.conditions.push({
|
||||
id: uuid4(),
|
||||
varType: variable.type,
|
||||
variable_selector: valueSelector,
|
||||
comparison_operator: getOperators(variable.type, isVarFileAttribute ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
|
||||
value: (variable.type === VarType.boolean || variable.type === VarType.arrayBoolean) ? false : '',
|
||||
})
|
||||
})
|
||||
|
||||
export const removeCondition = (
|
||||
inputs: IfElseNodeType,
|
||||
caseId: string,
|
||||
conditionId: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (targetCase)
|
||||
targetCase.conditions = targetCase.conditions.filter(item => item.id !== conditionId)
|
||||
})
|
||||
|
||||
export const updateCondition = (
|
||||
inputs: IfElseNodeType,
|
||||
caseId: string,
|
||||
conditionId: string,
|
||||
nextCondition: Condition,
|
||||
) => produce(inputs, (draft) => {
|
||||
const targetCondition = draft.cases
|
||||
?.find(item => item.case_id === caseId)
|
||||
?.conditions
|
||||
.find(item => item.id === conditionId)
|
||||
|
||||
if (targetCondition)
|
||||
Object.assign(targetCondition, nextCondition)
|
||||
})
|
||||
|
||||
export const toggleConditionLogicalOperator = (
|
||||
inputs: IfElseNodeType,
|
||||
caseId: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (!targetCase)
|
||||
return
|
||||
|
||||
targetCase.logical_operator = targetCase.logical_operator === LogicalOperator.and
|
||||
? LogicalOperator.or
|
||||
: LogicalOperator.and
|
||||
})
|
||||
|
||||
export const addSubVariableCondition = (
|
||||
inputs: IfElseNodeType,
|
||||
caseId: string,
|
||||
conditionId: string,
|
||||
key?: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
const condition = draft.cases
|
||||
?.find(item => item.case_id === caseId)
|
||||
?.conditions
|
||||
.find(item => item.id === conditionId)
|
||||
|
||||
if (!condition)
|
||||
return
|
||||
|
||||
if (!condition.sub_variable_condition) {
|
||||
condition.sub_variable_condition = {
|
||||
case_id: uuid4(),
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
}
|
||||
}
|
||||
|
||||
condition.sub_variable_condition.conditions.push({
|
||||
id: uuid4(),
|
||||
key: key || '',
|
||||
varType: VarType.string,
|
||||
comparison_operator: undefined,
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
|
||||
export const removeSubVariableCondition = (
|
||||
inputs: IfElseNodeType,
|
||||
caseId: string,
|
||||
conditionId: string,
|
||||
subConditionId: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
const subVariableCondition = draft.cases
|
||||
?.find(item => item.case_id === caseId)
|
||||
?.conditions
|
||||
.find(item => item.id === conditionId)
|
||||
?.sub_variable_condition
|
||||
|
||||
if (!subVariableCondition)
|
||||
return
|
||||
|
||||
subVariableCondition.conditions = subVariableCondition.conditions.filter(item => item.id !== subConditionId)
|
||||
})
|
||||
|
||||
export const updateSubVariableCondition = (
|
||||
inputs: IfElseNodeType,
|
||||
caseId: string,
|
||||
conditionId: string,
|
||||
subConditionId: string,
|
||||
nextCondition: Condition,
|
||||
) => produce(inputs, (draft) => {
|
||||
const targetSubCondition = draft.cases
|
||||
?.find(item => item.case_id === caseId)
|
||||
?.conditions
|
||||
.find(item => item.id === conditionId)
|
||||
?.sub_variable_condition
|
||||
?.conditions
|
||||
.find(item => item.id === subConditionId)
|
||||
|
||||
if (targetSubCondition)
|
||||
Object.assign(targetSubCondition, nextCondition)
|
||||
})
|
||||
|
||||
export const toggleSubVariableConditionLogicalOperator = (
|
||||
inputs: IfElseNodeType,
|
||||
caseId: string,
|
||||
conditionId: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
const targetSubVariableCondition = draft.cases
|
||||
?.find(item => item.case_id === caseId)
|
||||
?.conditions
|
||||
.find(item => item.id === conditionId)
|
||||
?.sub_variable_condition
|
||||
|
||||
if (!targetSubVariableCondition)
|
||||
return
|
||||
|
||||
targetSubVariableCondition.logical_operator = targetSubVariableCondition.logical_operator === LogicalOperator.and
|
||||
? LogicalOperator.or
|
||||
: LogicalOperator.and
|
||||
})
|
||||
@ -12,33 +12,48 @@ import type {
|
||||
HandleUpdateSubVariableCondition,
|
||||
IfElseNodeType,
|
||||
} from './types'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useUpdateNodeInternals } from 'reactflow'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import {
|
||||
useEdgesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import { VarType } from '../../types'
|
||||
import { LogicalOperator } from './types'
|
||||
import useIsVarFileAttribute from './use-is-var-file-attribute'
|
||||
import {
|
||||
branchNameCorrect,
|
||||
getOperators,
|
||||
} from './utils'
|
||||
addCase,
|
||||
addCondition,
|
||||
addSubVariableCondition,
|
||||
filterAllVars,
|
||||
filterNumberVars,
|
||||
getVarsIsVarFileAttribute,
|
||||
removeCase,
|
||||
removeCondition,
|
||||
removeSubVariableCondition,
|
||||
sortCases,
|
||||
toggleConditionLogicalOperator,
|
||||
toggleSubVariableConditionLogicalOperator,
|
||||
updateCondition,
|
||||
updateSubVariableCondition,
|
||||
} from './use-config.helpers'
|
||||
import useIsVarFileAttribute from './use-is-var-file-attribute'
|
||||
|
||||
const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
|
||||
const { inputs, setInputs } = useNodeCrud<IfElseNodeType>(id, payload)
|
||||
const inputsRef = useRef(inputs)
|
||||
const handleInputsChange = useCallback((newInputs: IfElseNodeType) => {
|
||||
inputsRef.current = newInputs
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const filterVar = useCallback(() => {
|
||||
return true
|
||||
}, [])
|
||||
const filterVar = useCallback(() => filterAllVars(), [])
|
||||
|
||||
const {
|
||||
availableVars,
|
||||
@ -48,9 +63,7 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
filterVar,
|
||||
})
|
||||
|
||||
const filterNumberVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type === VarType.number
|
||||
}, [])
|
||||
const filterNumberVar = useCallback((varPayload: Var) => filterNumberVars(varPayload), [])
|
||||
|
||||
const {
|
||||
getIsVarFileAttribute,
|
||||
@ -61,13 +74,7 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
})
|
||||
|
||||
const varsIsVarFileAttribute = useMemo(() => {
|
||||
const conditions: Record<string, boolean> = {}
|
||||
inputs.cases?.forEach((c) => {
|
||||
c.conditions.forEach((condition) => {
|
||||
conditions[condition.id] = getIsVarFileAttribute(condition.variable_selector!)
|
||||
})
|
||||
})
|
||||
return conditions
|
||||
return getVarsIsVarFileAttribute(inputs.cases, getIsVarFileAttribute)
|
||||
}, [inputs.cases, getIsVarFileAttribute])
|
||||
|
||||
const {
|
||||
@ -79,177 +86,56 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
})
|
||||
|
||||
const handleAddCase = useCallback(() => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
if (draft.cases) {
|
||||
const case_id = uuid4()
|
||||
draft.cases.push({
|
||||
case_id,
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
})
|
||||
if (draft._targetBranches) {
|
||||
const elseCaseIndex = draft._targetBranches.findIndex(branch => branch.id === 'false')
|
||||
if (elseCaseIndex > -1) {
|
||||
draft._targetBranches = branchNameCorrect([
|
||||
...draft._targetBranches.slice(0, elseCaseIndex),
|
||||
{
|
||||
id: case_id,
|
||||
name: '',
|
||||
},
|
||||
...draft._targetBranches.slice(elseCaseIndex),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
handleInputsChange(addCase(inputsRef.current))
|
||||
}, [handleInputsChange])
|
||||
|
||||
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])
|
||||
handleEdgeDeleteByDeleteBranch(id, caseId)
|
||||
handleInputsChange(removeCase(inputsRef.current, caseId))
|
||||
}, [handleEdgeDeleteByDeleteBranch, handleInputsChange, id])
|
||||
|
||||
const handleSortCase = useCallback((newCases: (CaseItem & { id: string })[]) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
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)
|
||||
handleInputsChange(sortCases(inputsRef.current, newCases))
|
||||
updateNodeInternals(id)
|
||||
}, [id, inputs, setInputs, updateNodeInternals])
|
||||
}, [handleInputsChange, id, updateNodeInternals])
|
||||
|
||||
const handleAddCondition = useCallback<HandleAddCondition>((caseId, valueSelector, varItem) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
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, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
|
||||
value: (varItem.type === VarType.boolean || varItem.type === VarType.arrayBoolean) ? false : '',
|
||||
})
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [getIsVarFileAttribute, inputs, setInputs])
|
||||
handleInputsChange(addCondition({
|
||||
inputs: inputsRef.current,
|
||||
caseId,
|
||||
valueSelector,
|
||||
variable: varItem,
|
||||
isVarFileAttribute: !!getIsVarFileAttribute(valueSelector),
|
||||
}))
|
||||
}, [getIsVarFileAttribute, handleInputsChange])
|
||||
|
||||
const handleRemoveCondition = useCallback<HandleRemoveCondition>((caseId, conditionId) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
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])
|
||||
handleInputsChange(removeCondition(inputsRef.current, caseId, conditionId))
|
||||
}, [handleInputsChange])
|
||||
|
||||
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])
|
||||
handleInputsChange(updateCondition(inputsRef.current, caseId, conditionId, newCondition))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>((caseId) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (targetCase)
|
||||
targetCase.logical_operator = targetCase.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
handleInputsChange(toggleConditionLogicalOperator(inputsRef.current, caseId))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleAddSubVariableCondition = useCallback<HandleAddSubVariableCondition>((caseId: string, conditionId: string, key?: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const condition = draft.cases?.find(item => item.case_id === caseId)?.conditions.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
if (!condition?.sub_variable_condition) {
|
||||
condition.sub_variable_condition = {
|
||||
case_id: uuid4(),
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
}
|
||||
}
|
||||
const subVarCondition = condition.sub_variable_condition
|
||||
if (subVarCondition) {
|
||||
if (!subVarCondition.conditions)
|
||||
subVarCondition.conditions = []
|
||||
|
||||
subVarCondition.conditions.push({
|
||||
id: uuid4(),
|
||||
key: key || '',
|
||||
varType: VarType.string,
|
||||
comparison_operator: undefined,
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
handleInputsChange(addSubVariableCondition(inputsRef.current, caseId, conditionId, key))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleRemoveSubVariableCondition = useCallback((caseId: string, conditionId: string, subConditionId: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const condition = draft.cases?.find(item => item.case_id === caseId)?.conditions.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
if (!condition?.sub_variable_condition)
|
||||
return
|
||||
const subVarCondition = condition.sub_variable_condition
|
||||
if (subVarCondition)
|
||||
subVarCondition.conditions = subVarCondition.conditions.filter(item => item.id !== subConditionId)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
handleInputsChange(removeSubVariableCondition(inputsRef.current, caseId, conditionId, subConditionId))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateSubVariableCondition = useCallback<HandleUpdateSubVariableCondition>((caseId, conditionId, subConditionId, newSubCondition) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (targetCase) {
|
||||
const targetCondition = targetCase.conditions.find(item => item.id === conditionId)
|
||||
if (targetCondition && targetCondition.sub_variable_condition) {
|
||||
const targetSubCondition = targetCondition.sub_variable_condition.conditions.find(item => item.id === subConditionId)
|
||||
if (targetSubCondition)
|
||||
Object.assign(targetSubCondition, newSubCondition)
|
||||
}
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
handleInputsChange(updateSubVariableCondition(inputsRef.current, caseId, conditionId, subConditionId, newSubCondition))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleToggleSubVariableConditionLogicalOperator = useCallback<HandleToggleSubVariableConditionLogicalOperator>((caseId, conditionId) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (targetCase) {
|
||||
const targetCondition = targetCase.conditions.find(item => item.id === conditionId)
|
||||
if (targetCondition && targetCondition.sub_variable_condition)
|
||||
targetCondition.sub_variable_condition.logical_operator = targetCondition.sub_variable_condition.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
handleInputsChange(toggleSubVariableConditionLogicalOperator(inputsRef.current, caseId, conditionId))
|
||||
}, [handleInputsChange])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
|
||||
Reference in New Issue
Block a user