mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 00:48:04 +08:00
Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox
This commit is contained in:
@ -0,0 +1,216 @@
|
||||
import type { LoopNodeType } from '../types'
|
||||
import { BlockEnum, ErrorHandleMode, ValueType, VarType } from '@/app/components/workflow/types'
|
||||
import { createUuidModuleMock } from '../../__tests__/use-config-test-utils'
|
||||
import { ComparisonOperator, LogicalOperator } from '../types'
|
||||
import {
|
||||
addBreakCondition,
|
||||
addLoopVariable,
|
||||
addSubVariableCondition,
|
||||
canUseAsLoopInput,
|
||||
removeBreakCondition,
|
||||
removeLoopVariable,
|
||||
removeSubVariableCondition,
|
||||
toggleConditionOperator,
|
||||
toggleSubVariableConditionOperator,
|
||||
updateBreakCondition,
|
||||
updateErrorHandleMode,
|
||||
updateLoopCount,
|
||||
updateLoopVariable,
|
||||
updateSubVariableCondition,
|
||||
} from '../use-config.helpers'
|
||||
|
||||
const mockUuid = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('uuid', () => createUuidModuleMock(() => mockUuid()))
|
||||
|
||||
const createInputs = (overrides: Partial<LoopNodeType> = {}): LoopNodeType => ({
|
||||
title: 'Loop',
|
||||
desc: '',
|
||||
type: BlockEnum.Loop,
|
||||
start_node_id: 'start-node',
|
||||
loop_count: 3,
|
||||
error_handle_mode: ErrorHandleMode.Terminated,
|
||||
logical_operator: LogicalOperator.and,
|
||||
break_conditions: [],
|
||||
loop_variables: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('loop use-config helpers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('canUseAsLoopInput', () => {
|
||||
it.each([
|
||||
VarType.array,
|
||||
VarType.arrayString,
|
||||
VarType.arrayNumber,
|
||||
VarType.arrayObject,
|
||||
VarType.arrayFile,
|
||||
])('should accept %s loop inputs', (type) => {
|
||||
expect(canUseAsLoopInput({ type } as never)).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject non-array loop inputs', () => {
|
||||
expect(canUseAsLoopInput({ type: VarType.string } as never)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update error handling, loop count and logical operators immutably', () => {
|
||||
const inputs = createInputs()
|
||||
|
||||
const withMode = updateErrorHandleMode(inputs, ErrorHandleMode.ContinueOnError)
|
||||
const withCount = updateLoopCount(withMode, 6)
|
||||
const toggled = toggleConditionOperator(withCount)
|
||||
const toggledBack = toggleConditionOperator(toggled)
|
||||
|
||||
expect(withMode.error_handle_mode).toBe(ErrorHandleMode.ContinueOnError)
|
||||
expect(withCount.loop_count).toBe(6)
|
||||
expect(toggled.logical_operator).toBe(LogicalOperator.or)
|
||||
expect(toggledBack.logical_operator).toBe(LogicalOperator.and)
|
||||
expect(inputs.error_handle_mode).toBe(ErrorHandleMode.Terminated)
|
||||
expect(inputs.loop_count).toBe(3)
|
||||
})
|
||||
|
||||
it('should add, update and remove break conditions for regular and file attributes', () => {
|
||||
mockUuid
|
||||
.mockReturnValueOnce('condition-1')
|
||||
.mockReturnValueOnce('condition-2')
|
||||
|
||||
const withBooleanCondition = addBreakCondition({
|
||||
inputs: createInputs({ break_conditions: undefined }),
|
||||
valueSelector: ['tool-node', 'enabled'],
|
||||
variable: { type: VarType.boolean },
|
||||
isVarFileAttribute: false,
|
||||
})
|
||||
const withFileCondition = addBreakCondition({
|
||||
inputs: withBooleanCondition,
|
||||
valueSelector: ['tool-node', 'file', 'transfer_method'],
|
||||
variable: { type: VarType.file },
|
||||
isVarFileAttribute: true,
|
||||
})
|
||||
const updated = updateBreakCondition(withFileCondition, 'condition-2', {
|
||||
id: 'condition-2',
|
||||
varType: VarType.file,
|
||||
key: 'transfer_method',
|
||||
variable_selector: ['tool-node', 'file', 'transfer_method'],
|
||||
comparison_operator: ComparisonOperator.notIn,
|
||||
value: [VarType.file],
|
||||
})
|
||||
const removed = removeBreakCondition(updated, 'condition-1')
|
||||
|
||||
expect(withBooleanCondition.break_conditions).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'condition-1',
|
||||
varType: VarType.boolean,
|
||||
comparison_operator: ComparisonOperator.is,
|
||||
value: 'false',
|
||||
}),
|
||||
])
|
||||
expect(withFileCondition.break_conditions?.[1]).toEqual(expect.objectContaining({
|
||||
id: 'condition-2',
|
||||
varType: VarType.file,
|
||||
comparison_operator: ComparisonOperator.in,
|
||||
value: '',
|
||||
}))
|
||||
expect(updated.break_conditions?.[1]).toEqual(expect.objectContaining({
|
||||
comparison_operator: ComparisonOperator.notIn,
|
||||
value: [VarType.file],
|
||||
}))
|
||||
expect(removed.break_conditions).toEqual([
|
||||
expect.objectContaining({ id: 'condition-2' }),
|
||||
])
|
||||
})
|
||||
|
||||
it('should manage nested sub-variable conditions and ignore missing targets', () => {
|
||||
mockUuid
|
||||
.mockReturnValueOnce('sub-condition-1')
|
||||
.mockReturnValueOnce('sub-condition-2')
|
||||
|
||||
const inputs = createInputs({
|
||||
break_conditions: [{
|
||||
id: 'condition-1',
|
||||
varType: VarType.file,
|
||||
key: 'name',
|
||||
variable_selector: ['tool-node', 'file'],
|
||||
comparison_operator: ComparisonOperator.contains,
|
||||
value: '',
|
||||
}],
|
||||
})
|
||||
|
||||
const untouched = addSubVariableCondition(inputs, 'missing-condition')
|
||||
const withKeyedSubCondition = addSubVariableCondition(inputs, 'condition-1', 'transfer_method')
|
||||
const withDefaultKeySubCondition = addSubVariableCondition(withKeyedSubCondition, 'condition-1')
|
||||
const updated = updateSubVariableCondition(withDefaultKeySubCondition, 'condition-1', 'sub-condition-1', {
|
||||
id: 'sub-condition-1',
|
||||
key: 'transfer_method',
|
||||
varType: VarType.string,
|
||||
comparison_operator: ComparisonOperator.notIn,
|
||||
value: ['remote_url'],
|
||||
})
|
||||
const toggled = toggleSubVariableConditionOperator(updated, 'condition-1')
|
||||
const removed = removeSubVariableCondition(toggled, 'condition-1', 'sub-condition-1')
|
||||
const unchangedAfterMissingRemove = removeSubVariableCondition(removed, 'missing-condition', 'sub-condition-2')
|
||||
|
||||
expect(untouched).toEqual(inputs)
|
||||
expect(withKeyedSubCondition.break_conditions?.[0].sub_variable_condition).toEqual({
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [{
|
||||
id: 'sub-condition-1',
|
||||
key: 'transfer_method',
|
||||
varType: VarType.string,
|
||||
comparison_operator: ComparisonOperator.in,
|
||||
value: '',
|
||||
}],
|
||||
})
|
||||
expect(withDefaultKeySubCondition.break_conditions?.[0].sub_variable_condition?.conditions[1]).toEqual({
|
||||
id: 'sub-condition-2',
|
||||
key: '',
|
||||
varType: VarType.string,
|
||||
comparison_operator: undefined,
|
||||
value: '',
|
||||
})
|
||||
expect(updated.break_conditions?.[0].sub_variable_condition?.conditions[0]).toEqual(expect.objectContaining({
|
||||
comparison_operator: ComparisonOperator.notIn,
|
||||
value: ['remote_url'],
|
||||
}))
|
||||
expect(toggled.break_conditions?.[0].sub_variable_condition?.logical_operator).toBe(LogicalOperator.or)
|
||||
expect(removed.break_conditions?.[0].sub_variable_condition?.conditions).toEqual([
|
||||
expect.objectContaining({ id: 'sub-condition-2' }),
|
||||
])
|
||||
expect(unchangedAfterMissingRemove).toEqual(removed)
|
||||
})
|
||||
|
||||
it('should add, update and remove loop variables without mutating the source inputs', () => {
|
||||
mockUuid.mockReturnValueOnce('loop-variable-1')
|
||||
|
||||
const inputs = createInputs({ loop_variables: undefined })
|
||||
const added = addLoopVariable(inputs)
|
||||
const updated = updateLoopVariable(added, 'loop-variable-1', {
|
||||
label: 'Loop Value',
|
||||
value_type: ValueType.variable,
|
||||
value: ['tool-node', 'result'],
|
||||
})
|
||||
const unchanged = updateLoopVariable(updated, 'missing-loop-variable', { label: 'ignored' })
|
||||
const removed = removeLoopVariable(unchanged, 'loop-variable-1')
|
||||
|
||||
expect(added.loop_variables).toEqual([{
|
||||
id: 'loop-variable-1',
|
||||
label: '',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.constant,
|
||||
value: '',
|
||||
}])
|
||||
expect(updated.loop_variables).toEqual([{
|
||||
id: 'loop-variable-1',
|
||||
label: 'Loop Value',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.variable,
|
||||
value: ['tool-node', 'result'],
|
||||
}])
|
||||
expect(unchanged).toEqual(updated)
|
||||
expect(removed.loop_variables).toEqual([])
|
||||
expect(inputs.loop_variables).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,221 @@
|
||||
import type { LoopNodeType } from '../types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { BlockEnum, ErrorHandleMode, ValueType, 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 mockGetLoopNodeChildren = 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('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: { conversationVariables: unknown[], dataSourceList: unknown[] }) => unknown) => selector({
|
||||
conversationVariables: [],
|
||||
dataSourceList: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useAllBuiltInTools: () => ({ data: [] }),
|
||||
useAllCustomTools: () => ({ data: [] }),
|
||||
useAllWorkflowTools: () => ({ data: [] }),
|
||||
useAllMCPTools: () => ({ data: [] }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useNodesReadOnly: () => ({ nodesReadOnly: false }),
|
||||
useIsChatMode: () => false,
|
||||
useWorkflow: () => ({
|
||||
getLoopNodeChildren: (...args: unknown[]) => mockGetLoopNodeChildren(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
|
||||
...createNodeCrudModuleMock<LoopNodeType>(mockSetInputs),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/variable/utils', () => ({
|
||||
toNodeOutputVars: () => [{ nodeId: 'child-node', title: 'Child', vars: [] }],
|
||||
}))
|
||||
|
||||
vi.mock('../use-is-var-file-attribute', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
getIsVarFileAttribute: (...args: unknown[]) => mockGetIsVarFileAttribute(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
const createPayload = (overrides: Partial<LoopNodeType> = {}): LoopNodeType => ({
|
||||
title: 'Loop',
|
||||
desc: '',
|
||||
type: BlockEnum.Loop,
|
||||
start_node_id: 'start-node',
|
||||
loop_id: 'loop-node',
|
||||
logical_operator: LogicalOperator.and,
|
||||
break_conditions: [{
|
||||
id: 'condition-1',
|
||||
varType: VarType.string,
|
||||
variable_selector: ['node-1', 'answer'],
|
||||
comparison_operator: ComparisonOperator.contains,
|
||||
value: 'hello',
|
||||
}],
|
||||
loop_count: 3,
|
||||
error_handle_mode: ErrorHandleMode.ContinueOnError,
|
||||
loop_variables: [{
|
||||
id: 'loop-var-1',
|
||||
label: 'item',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.constant,
|
||||
value: 'value',
|
||||
}],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('useConfig', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetLoopNodeChildren.mockReturnValue([])
|
||||
mockGetIsVarFileAttribute.mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('should expose derived outputs and input variable filtering', () => {
|
||||
const { result } = renderHook(() => useConfig('loop-node', createPayload()))
|
||||
|
||||
expect(result.current.readOnly).toBe(false)
|
||||
expect(result.current.childrenNodeVars).toEqual([{ nodeId: 'child-node', title: 'Child', vars: [] }])
|
||||
expect(result.current.loopChildrenNodes).toHaveLength(1)
|
||||
expect(result.current.filterInputVar({ type: VarType.arrayNumber } as never)).toBe(true)
|
||||
expect(result.current.filterInputVar({ type: VarType.string } as never)).toBe(false)
|
||||
})
|
||||
|
||||
it('should update error mode, break conditions and logical operators', () => {
|
||||
const { result } = renderHook(() => useConfig('loop-node', createPayload()))
|
||||
|
||||
result.current.changeErrorResponseMode({ value: ErrorHandleMode.Terminated })
|
||||
result.current.handleAddCondition(['node-1', 'score'], { type: VarType.number } as never)
|
||||
result.current.handleUpdateCondition('condition-1', {
|
||||
id: 'condition-1',
|
||||
varType: VarType.number,
|
||||
variable_selector: ['node-1', 'score'],
|
||||
comparison_operator: ComparisonOperator.largerThan,
|
||||
value: '3',
|
||||
})
|
||||
result.current.handleRemoveCondition('condition-1')
|
||||
result.current.handleToggleConditionLogicalOperator()
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
error_handle_mode: ErrorHandleMode.Terminated,
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
break_conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'generated-id',
|
||||
variable_selector: ['node-1', 'score'],
|
||||
varType: VarType.number,
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
break_conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
varType: VarType.number,
|
||||
comparison_operator: ComparisonOperator.largerThan,
|
||||
value: '3',
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
logical_operator: LogicalOperator.or,
|
||||
}))
|
||||
})
|
||||
|
||||
it('should manage sub-variable conditions and loop variables', () => {
|
||||
const payload = createPayload({
|
||||
break_conditions: [{
|
||||
id: 'condition-1',
|
||||
varType: VarType.file,
|
||||
variable_selector: ['node-1', 'files'],
|
||||
comparison_operator: ComparisonOperator.contains,
|
||||
value: '',
|
||||
sub_variable_condition: {
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [{
|
||||
id: 'sub-1',
|
||||
key: 'name',
|
||||
varType: VarType.string,
|
||||
comparison_operator: ComparisonOperator.contains,
|
||||
value: '',
|
||||
}],
|
||||
},
|
||||
}],
|
||||
})
|
||||
const { result } = renderHook(() => useConfig('loop-node', payload))
|
||||
|
||||
result.current.handleAddSubVariableCondition('condition-1', 'name')
|
||||
result.current.handleUpdateSubVariableCondition('condition-1', 'sub-1', {
|
||||
id: 'sub-1',
|
||||
key: 'size',
|
||||
varType: VarType.string,
|
||||
comparison_operator: ComparisonOperator.contains,
|
||||
value: '2',
|
||||
})
|
||||
result.current.handleRemoveSubVariableCondition('condition-1', 'sub-1')
|
||||
result.current.handleToggleSubVariableConditionLogicalOperator('condition-1')
|
||||
result.current.handleUpdateLoopCount(5)
|
||||
result.current.handleAddLoopVariable()
|
||||
result.current.handleRemoveLoopVariable('loop-var-1')
|
||||
result.current.handleUpdateLoopVariable('loop-var-1', { label: 'updated' })
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
break_conditions: [
|
||||
expect.objectContaining({
|
||||
sub_variable_condition: expect.objectContaining({
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'generated-id',
|
||||
key: 'name',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
break_conditions: [
|
||||
expect.objectContaining({
|
||||
sub_variable_condition: expect.objectContaining({
|
||||
logical_operator: LogicalOperator.or,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
loop_count: 5,
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
loop_variables: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'generated-id',
|
||||
value_type: ValueType.constant,
|
||||
}),
|
||||
]),
|
||||
}))
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
loop_variables: [
|
||||
expect.objectContaining({
|
||||
id: 'generated-id',
|
||||
value_type: ValueType.constant,
|
||||
}),
|
||||
],
|
||||
}))
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,100 @@
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import {
|
||||
buildLoopChildCopy,
|
||||
getContainerBounds,
|
||||
getContainerResize,
|
||||
getLoopChildren,
|
||||
getRestrictedLoopPosition,
|
||||
} from '../use-interactions.helpers'
|
||||
|
||||
const createNode = (overrides: Record<string, unknown> = {}) => ({
|
||||
id: 'node',
|
||||
type: 'custom',
|
||||
position: { x: 0, y: 0 },
|
||||
width: 100,
|
||||
height: 80,
|
||||
data: { type: BlockEnum.Code, title: 'Code', desc: '' },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('loop interaction helpers', () => {
|
||||
it('calculates bounds and container resize from overflowing children', () => {
|
||||
const children = [
|
||||
createNode({ id: 'a', position: { x: 20, y: 10 }, width: 80, height: 40 }),
|
||||
createNode({ id: 'b', position: { x: 120, y: 60 }, width: 50, height: 30 }),
|
||||
]
|
||||
|
||||
const bounds = getContainerBounds(children as Node[])
|
||||
expect(bounds.rightNode?.id).toBe('b')
|
||||
expect(bounds.bottomNode?.id).toBe('b')
|
||||
expect(getContainerResize(createNode({ width: 120, height: 80 }) as Node, bounds)).toEqual({
|
||||
width: 186,
|
||||
height: 110,
|
||||
})
|
||||
expect(getContainerResize(createNode({ width: 300, height: 300 }), bounds)).toEqual({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('restricts loop positions only for loop children and filters loop-start nodes', () => {
|
||||
const parent = createNode({ id: 'parent', width: 200, height: 180 })
|
||||
expect(getRestrictedLoopPosition(createNode({ data: { isInLoop: false } }) as Node, parent as Node)).toEqual({ x: undefined, y: undefined })
|
||||
expect(getRestrictedLoopPosition(
|
||||
createNode({
|
||||
position: { x: -10, y: 160 },
|
||||
width: 80,
|
||||
height: 40,
|
||||
data: { isInLoop: true },
|
||||
}),
|
||||
parent as Node,
|
||||
)).toEqual({ x: 16, y: 120 })
|
||||
expect(getRestrictedLoopPosition(
|
||||
createNode({
|
||||
position: { x: 180, y: -4 },
|
||||
width: 40,
|
||||
height: 30,
|
||||
data: { isInLoop: true },
|
||||
}),
|
||||
parent as Node,
|
||||
)).toEqual({ x: 144, y: 65 })
|
||||
expect(getLoopChildren([
|
||||
createNode({ id: 'child', parentId: 'loop-1' }),
|
||||
createNode({ id: 'start', parentId: 'loop-1', type: 'custom-loop-start' }),
|
||||
createNode({ id: 'other', parentId: 'other-loop' }),
|
||||
] as Node[], 'loop-1').map(item => item.id)).toEqual(['child'])
|
||||
})
|
||||
|
||||
it('builds copied loop children with derived title and loop metadata', () => {
|
||||
const child = createNode({
|
||||
id: 'child',
|
||||
position: { x: 12, y: 24 },
|
||||
positionAbsolute: { x: 12, y: 24 },
|
||||
extent: 'parent',
|
||||
data: { type: BlockEnum.Code, title: 'Original', desc: 'child', selected: true },
|
||||
})
|
||||
|
||||
const result = buildLoopChildCopy({
|
||||
child: child as Node,
|
||||
childNodeType: BlockEnum.Code,
|
||||
defaultValue: { title: 'Code', desc: '', type: BlockEnum.Code } as Node['data'],
|
||||
nodesWithSameTypeCount: 2,
|
||||
newNodeId: 'loop-2',
|
||||
index: 3,
|
||||
})
|
||||
|
||||
expect(result.newId).toBe('loop-23')
|
||||
expect(result.params).toEqual(expect.objectContaining({
|
||||
parentId: 'loop-2',
|
||||
zIndex: 1002,
|
||||
data: expect.objectContaining({
|
||||
title: 'Code 3',
|
||||
isInLoop: true,
|
||||
loop_id: 'loop-2',
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,174 @@
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import {
|
||||
createLoopNode,
|
||||
createNode,
|
||||
} from '@/app/components/workflow/__tests__/fixtures'
|
||||
import { LOOP_PADDING } from '@/app/components/workflow/constants'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useNodeLoopInteractions } from '../use-interactions'
|
||||
|
||||
const mockGetNodes = vi.hoisted(() => vi.fn())
|
||||
const mockSetNodes = vi.hoisted(() => vi.fn())
|
||||
const mockGenerateNewNode = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('reactflow', async () => {
|
||||
const actual = await vi.importActual<typeof import('reactflow')>('reactflow')
|
||||
return {
|
||||
...actual,
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
setNodes: mockSetNodes,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useNodesMetaData: () => ({
|
||||
nodesMap: {
|
||||
[BlockEnum.Code]: {
|
||||
defaultValue: {
|
||||
title: 'Code',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', () => ({
|
||||
generateNewNode: (...args: unknown[]) => mockGenerateNewNode(...args),
|
||||
getNodeCustomTypeByNodeDataType: () => 'custom',
|
||||
}))
|
||||
|
||||
describe('useNodeLoopInteractions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should expand the loop node when children overflow the bounds', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createLoopNode({
|
||||
id: 'loop-node',
|
||||
width: 120,
|
||||
height: 80,
|
||||
data: { width: 120, height: 80 },
|
||||
}),
|
||||
createNode({
|
||||
id: 'child-node',
|
||||
parentId: 'loop-node',
|
||||
position: { x: 100, y: 90 },
|
||||
width: 60,
|
||||
height: 40,
|
||||
}),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useNodeLoopInteractions())
|
||||
result.current.handleNodeLoopRerender('loop-node')
|
||||
|
||||
expect(mockSetNodes).toHaveBeenCalledTimes(1)
|
||||
const updatedNodes = mockSetNodes.mock.calls[0][0]
|
||||
const updatedLoopNode = updatedNodes.find((node: Node) => node.id === 'loop-node')
|
||||
expect(updatedLoopNode.width).toBe(100 + 60 + LOOP_PADDING.right)
|
||||
expect(updatedLoopNode.height).toBe(90 + 40 + LOOP_PADDING.bottom)
|
||||
})
|
||||
|
||||
it('should restrict dragging to the loop container padding', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createLoopNode({
|
||||
id: 'loop-node',
|
||||
width: 200,
|
||||
height: 180,
|
||||
data: { width: 200, height: 180 },
|
||||
}),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useNodeLoopInteractions())
|
||||
const dragResult = result.current.handleNodeLoopChildDrag(createNode({
|
||||
id: 'child-node',
|
||||
parentId: 'loop-node',
|
||||
position: { x: -10, y: -5 },
|
||||
width: 80,
|
||||
height: 60,
|
||||
data: { type: BlockEnum.Code, title: 'Child', desc: '', isInLoop: true },
|
||||
}))
|
||||
|
||||
expect(dragResult.restrictPosition).toEqual({
|
||||
x: LOOP_PADDING.left,
|
||||
y: LOOP_PADDING.top,
|
||||
})
|
||||
})
|
||||
|
||||
it('should rerender the parent loop node when a child size changes', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createLoopNode({
|
||||
id: 'loop-node',
|
||||
width: 120,
|
||||
height: 80,
|
||||
data: { width: 120, height: 80 },
|
||||
}),
|
||||
createNode({
|
||||
id: 'child-node',
|
||||
parentId: 'loop-node',
|
||||
position: { x: 100, y: 90 },
|
||||
width: 60,
|
||||
height: 40,
|
||||
}),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useNodeLoopInteractions())
|
||||
result.current.handleNodeLoopChildSizeChange('child-node')
|
||||
|
||||
expect(mockSetNodes).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should skip loop rerender when the resized node has no parent', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createNode({
|
||||
id: 'standalone-node',
|
||||
data: { type: BlockEnum.Code, title: 'Standalone', desc: '' },
|
||||
}),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useNodeLoopInteractions())
|
||||
result.current.handleNodeLoopChildSizeChange('standalone-node')
|
||||
|
||||
expect(mockSetNodes).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should copy loop children and remap ids', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createLoopNode({ id: 'loop-node' }),
|
||||
createNode({
|
||||
id: 'child-node',
|
||||
parentId: 'loop-node',
|
||||
data: { type: BlockEnum.Code, title: 'Child', desc: '' },
|
||||
}),
|
||||
createNode({
|
||||
id: 'same-type-node',
|
||||
data: { type: BlockEnum.Code, title: 'Code', desc: '' },
|
||||
}),
|
||||
])
|
||||
mockGenerateNewNode.mockReturnValue({
|
||||
newNode: createNode({
|
||||
id: 'generated',
|
||||
parentId: 'new-loop',
|
||||
data: { type: BlockEnum.Code, title: 'Code 3', desc: '', isInLoop: true, loop_id: 'new-loop' },
|
||||
}),
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useNodeLoopInteractions())
|
||||
const copyResult = result.current.handleNodeLoopChildrenCopy('loop-node', 'new-loop', { existing: 'mapped' })
|
||||
|
||||
expect(mockGenerateNewNode).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'custom',
|
||||
parentId: 'new-loop',
|
||||
}))
|
||||
expect(copyResult.copyChildren).toHaveLength(1)
|
||||
expect(copyResult.newIdMapping).toEqual({
|
||||
'existing': 'mapped',
|
||||
'child-node': 'new-loopgeneratednew-loop0',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,241 @@
|
||||
import type { InputVar, Node, Variable } from '../../../types'
|
||||
import type { Condition } from '../types'
|
||||
import { BlockEnum, InputVarType, ValueType, VarType } from '@/app/components/workflow/types'
|
||||
import { VALUE_SELECTOR_DELIMITER } from '@/config'
|
||||
import { ComparisonOperator, LogicalOperator } from '../types'
|
||||
import {
|
||||
buildUsedOutVars,
|
||||
createInputVarValues,
|
||||
dedupeInputVars,
|
||||
getDependentVarsFromLoopPayload,
|
||||
getVarSelectorsFromCase,
|
||||
getVarSelectorsFromCondition,
|
||||
} from '../use-single-run-form-params.helpers'
|
||||
|
||||
const mockGetNodeInfoById = vi.hoisted(() => vi.fn())
|
||||
const mockGetNodeUsedVarPassToServerKey = vi.hoisted(() => vi.fn())
|
||||
const mockGetNodeUsedVars = vi.hoisted(() => vi.fn())
|
||||
const mockIsSystemVar = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../../_base/components/variable/utils', () => ({
|
||||
getNodeInfoById: (...args: unknown[]) => mockGetNodeInfoById(...args),
|
||||
getNodeUsedVarPassToServerKey: (...args: unknown[]) => mockGetNodeUsedVarPassToServerKey(...args),
|
||||
getNodeUsedVars: (...args: unknown[]) => mockGetNodeUsedVars(...args),
|
||||
isSystemVar: (...args: unknown[]) => mockIsSystemVar(...args),
|
||||
}))
|
||||
|
||||
const createNode = (id: string, title: string, type = BlockEnum.Tool): Node => ({
|
||||
id,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title,
|
||||
desc: '',
|
||||
type,
|
||||
},
|
||||
} as Node)
|
||||
|
||||
const createInputVar = (variable: string, label: InputVar['label'] = variable): InputVar => ({
|
||||
type: InputVarType.textInput,
|
||||
label,
|
||||
variable,
|
||||
required: false,
|
||||
})
|
||||
|
||||
const createCondition = (overrides: Partial<Condition> = {}): Condition => ({
|
||||
id: 'condition-1',
|
||||
varType: VarType.string,
|
||||
variable_selector: ['tool-node', 'value'],
|
||||
comparison_operator: ComparisonOperator.equal,
|
||||
value: '',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('use-single-run-form-params helpers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should collect var selectors from conditions and nested cases', () => {
|
||||
const nestedCondition = createCondition({
|
||||
variable_selector: ['tool-node', 'value'],
|
||||
sub_variable_condition: {
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [
|
||||
createCondition({
|
||||
id: 'sub-condition-1',
|
||||
variable_selector: ['start-node', 'answer'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getVarSelectorsFromCondition(nestedCondition)).toEqual([
|
||||
['tool-node', 'value'],
|
||||
['start-node', 'answer'],
|
||||
])
|
||||
expect(getVarSelectorsFromCase({
|
||||
logical_operator: LogicalOperator.or,
|
||||
conditions: [
|
||||
nestedCondition,
|
||||
createCondition({
|
||||
id: 'condition-2',
|
||||
variable_selector: ['other-node', 'result'],
|
||||
}),
|
||||
],
|
||||
})).toEqual([
|
||||
['tool-node', 'value'],
|
||||
['start-node', 'answer'],
|
||||
['other-node', 'result'],
|
||||
])
|
||||
})
|
||||
|
||||
it('should copy input values and dedupe duplicate or invalid input vars', () => {
|
||||
const source = {
|
||||
question: 'hello',
|
||||
retry: true,
|
||||
}
|
||||
|
||||
const values = createInputVarValues(source)
|
||||
const deduped = dedupeInputVars([
|
||||
createInputVar('tool-node.value'),
|
||||
createInputVar('tool-node.value'),
|
||||
undefined as unknown as InputVar,
|
||||
createInputVar('start-node.answer'),
|
||||
])
|
||||
|
||||
expect(values).toEqual(source)
|
||||
expect(values).not.toBe(source)
|
||||
expect(deduped).toEqual([
|
||||
createInputVar('tool-node.value'),
|
||||
createInputVar('start-node.answer'),
|
||||
])
|
||||
})
|
||||
|
||||
it('should build used output vars and pass-to-server keys while filtering loop-local selectors', () => {
|
||||
const startNode = createNode('start-node', 'Start Node', BlockEnum.Start)
|
||||
const sysNode = createNode('sys', 'System', BlockEnum.Start)
|
||||
const loopChildrenNodes = [
|
||||
createNode('tool-a', 'Tool A'),
|
||||
createNode('tool-b', 'Tool B'),
|
||||
createNode('current-node', 'Current Node'),
|
||||
createNode('inner-node', 'Inner Node'),
|
||||
]
|
||||
|
||||
mockGetNodeUsedVars.mockImplementation((node: Node) => {
|
||||
switch (node.id) {
|
||||
case 'tool-a':
|
||||
return [['sys', 'files']]
|
||||
case 'tool-b':
|
||||
return [['start-node', 'answer'], ['current-node', 'self'], ['inner-node', 'secret']]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
})
|
||||
mockGetNodeUsedVarPassToServerKey.mockImplementation((_node: Node, selector: string[]) => {
|
||||
return selector[0] === 'sys' ? ['sys_files', 'sys_files_backup'] : 'answer_key'
|
||||
})
|
||||
mockGetNodeInfoById.mockImplementation((nodes: Node[], id: string) => nodes.find(node => node.id === id))
|
||||
mockIsSystemVar.mockImplementation((selector: string[]) => selector[0] === 'sys')
|
||||
|
||||
const toVarInputs = vi.fn((variables: Variable[]) => variables.map(variable => createInputVar(
|
||||
variable.variable,
|
||||
variable.label as InputVar['label'],
|
||||
)))
|
||||
|
||||
const result = buildUsedOutVars({
|
||||
loopChildrenNodes,
|
||||
currentNodeId: 'current-node',
|
||||
canChooseVarNodes: [startNode, sysNode, ...loopChildrenNodes],
|
||||
isNodeInLoop: nodeId => nodeId === 'inner-node',
|
||||
toVarInputs,
|
||||
})
|
||||
|
||||
expect(toVarInputs).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
variable: 'sys.files',
|
||||
label: {
|
||||
nodeType: BlockEnum.Start,
|
||||
nodeName: 'System',
|
||||
variable: 'sys.files',
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
variable: 'start-node.answer',
|
||||
label: {
|
||||
nodeType: BlockEnum.Start,
|
||||
nodeName: 'Start Node',
|
||||
variable: 'answer',
|
||||
},
|
||||
}),
|
||||
])
|
||||
expect(result.usedOutVars).toEqual([
|
||||
createInputVar('sys.files', {
|
||||
nodeType: BlockEnum.Start,
|
||||
nodeName: 'System',
|
||||
variable: 'sys.files',
|
||||
}),
|
||||
createInputVar('start-node.answer', {
|
||||
nodeType: BlockEnum.Start,
|
||||
nodeName: 'Start Node',
|
||||
variable: 'answer',
|
||||
}),
|
||||
])
|
||||
expect(result.allVarObject).toEqual({
|
||||
[['sys.files', 'tool-a', 0].join(VALUE_SELECTOR_DELIMITER)]: {
|
||||
inSingleRunPassedKey: 'sys_files',
|
||||
},
|
||||
[['sys.files', 'tool-a', 1].join(VALUE_SELECTOR_DELIMITER)]: {
|
||||
inSingleRunPassedKey: 'sys_files_backup',
|
||||
},
|
||||
[['start-node.answer', 'tool-b', 0].join(VALUE_SELECTOR_DELIMITER)]: {
|
||||
inSingleRunPassedKey: 'answer_key',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should derive dependent vars from payload and filter current node references', () => {
|
||||
const dependentVars = getDependentVarsFromLoopPayload({
|
||||
nodeId: 'loop-node',
|
||||
usedOutVars: [
|
||||
createInputVar('start-node.answer'),
|
||||
createInputVar('loop-node.internal'),
|
||||
],
|
||||
breakConditions: [
|
||||
createCondition({
|
||||
variable_selector: ['tool-node', 'value'],
|
||||
sub_variable_condition: {
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [
|
||||
createCondition({
|
||||
id: 'sub-condition-1',
|
||||
variable_selector: ['loop-node', 'ignored'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
loopVariables: [
|
||||
{
|
||||
id: 'loop-variable-1',
|
||||
label: 'Loop Input',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.variable,
|
||||
value: ['tool-node', 'next'],
|
||||
},
|
||||
{
|
||||
id: 'loop-variable-2',
|
||||
label: 'Constant',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.constant,
|
||||
value: 'plain-text',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(dependentVars).toEqual([
|
||||
['start-node', 'answer'],
|
||||
['tool-node', 'value'],
|
||||
['tool-node', 'next'],
|
||||
])
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,216 @@
|
||||
import type { InputVar, Node } from '../../../types'
|
||||
import type { LoopNodeType } from '../types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { BlockEnum, ErrorHandleMode, InputVarType, ValueType, VarType } from '@/app/components/workflow/types'
|
||||
import { ComparisonOperator, LogicalOperator } from '../types'
|
||||
import useSingleRunFormParams from '../use-single-run-form-params'
|
||||
|
||||
const mockUseIsNodeInLoop = vi.hoisted(() => vi.fn())
|
||||
const mockUseWorkflow = vi.hoisted(() => vi.fn())
|
||||
const mockFormatTracing = vi.hoisted(() => vi.fn())
|
||||
const mockGetNodeUsedVars = vi.hoisted(() => vi.fn())
|
||||
const mockGetNodeUsedVarPassToServerKey = vi.hoisted(() => vi.fn())
|
||||
const mockGetNodeInfoById = vi.hoisted(() => vi.fn())
|
||||
const mockIsSystemVar = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../../../hooks', () => ({
|
||||
useIsNodeInLoop: (...args: unknown[]) => mockUseIsNodeInLoop(...args),
|
||||
useWorkflow: () => mockUseWorkflow(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run/utils/format-log', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockFormatTracing(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../../_base/components/variable/utils', () => ({
|
||||
getNodeUsedVars: (...args: unknown[]) => mockGetNodeUsedVars(...args),
|
||||
getNodeUsedVarPassToServerKey: (...args: unknown[]) => mockGetNodeUsedVarPassToServerKey(...args),
|
||||
getNodeInfoById: (...args: unknown[]) => mockGetNodeInfoById(...args),
|
||||
isSystemVar: (...args: unknown[]) => mockIsSystemVar(...args),
|
||||
}))
|
||||
|
||||
const createLoopNode = (overrides: Partial<LoopNodeType> = {}): LoopNodeType => ({
|
||||
title: 'Loop',
|
||||
desc: '',
|
||||
type: BlockEnum.Loop,
|
||||
start_node_id: 'start-node',
|
||||
loop_count: 3,
|
||||
error_handle_mode: ErrorHandleMode.Terminated,
|
||||
break_conditions: [],
|
||||
loop_variables: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createVariableNode = (id: string, title: string, type = BlockEnum.Tool): Node => ({
|
||||
id,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title,
|
||||
type,
|
||||
desc: '',
|
||||
},
|
||||
} as Node)
|
||||
|
||||
const createInputVar = (variable: string): InputVar => ({
|
||||
type: InputVarType.textInput,
|
||||
label: variable,
|
||||
variable,
|
||||
required: false,
|
||||
})
|
||||
|
||||
const createRunTrace = (): NodeTracing => ({
|
||||
id: 'trace-1',
|
||||
index: 0,
|
||||
predecessor_node_id: '',
|
||||
node_id: 'loop-node',
|
||||
node_type: BlockEnum.Loop,
|
||||
title: 'Loop',
|
||||
inputs: {},
|
||||
inputs_truncated: false,
|
||||
process_data: {},
|
||||
process_data_truncated: false,
|
||||
outputs_truncated: false,
|
||||
status: 'succeeded',
|
||||
elapsed_time: 1,
|
||||
metadata: {
|
||||
iterator_length: 0,
|
||||
iterator_index: 0,
|
||||
loop_length: 2,
|
||||
loop_index: 1,
|
||||
},
|
||||
created_at: 0,
|
||||
created_by: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
email: 'user@example.com',
|
||||
},
|
||||
finished_at: 1,
|
||||
})
|
||||
|
||||
describe('useSingleRunFormParams', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseIsNodeInLoop.mockReturnValue({
|
||||
isNodeInLoop: (nodeId: string) => nodeId === 'inner-node',
|
||||
})
|
||||
mockUseWorkflow.mockReturnValue({
|
||||
getLoopNodeChildren: () => [
|
||||
createVariableNode('tool-a', 'Tool A'),
|
||||
createVariableNode('loop-node', 'Loop Node'),
|
||||
createVariableNode('inner-node', 'Inner Node'),
|
||||
],
|
||||
getBeforeNodesInSameBranch: () => [
|
||||
createVariableNode('start-node', 'Start Node', BlockEnum.Start),
|
||||
],
|
||||
})
|
||||
mockGetNodeUsedVars.mockImplementation((node: Node) => {
|
||||
if (node.id === 'tool-a')
|
||||
return [['start-node', 'answer']]
|
||||
if (node.id === 'loop-node')
|
||||
return [['loop-node', 'item']]
|
||||
if (node.id === 'inner-node')
|
||||
return [['inner-node', 'secret']]
|
||||
return []
|
||||
})
|
||||
mockGetNodeUsedVarPassToServerKey.mockReturnValue('passed_key')
|
||||
mockGetNodeInfoById.mockImplementation((nodes: Node[], id: string) => nodes.find(node => node.id === id))
|
||||
mockIsSystemVar.mockReturnValue(false)
|
||||
mockFormatTracing.mockReturnValue([{
|
||||
id: 'formatted-node',
|
||||
execution_metadata: { loop_index: 9 },
|
||||
}])
|
||||
})
|
||||
|
||||
it('should build single-run forms and filter out loop-local variables', () => {
|
||||
const toVarInputs = vi.fn((variables: Array<{ variable: string }>) => variables.map(item => createInputVar(item.variable)))
|
||||
const varSelectorsToVarInputs = vi.fn(() => [
|
||||
createInputVar('tool-a.result'),
|
||||
createInputVar('tool-a.result'),
|
||||
createInputVar('start-node.answer'),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useSingleRunFormParams({
|
||||
id: 'loop-node',
|
||||
payload: createLoopNode({
|
||||
break_conditions: [{
|
||||
id: 'condition-1',
|
||||
varType: VarType.string,
|
||||
variable_selector: ['tool-a', 'result'],
|
||||
comparison_operator: ComparisonOperator.equal,
|
||||
value: '',
|
||||
sub_variable_condition: {
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
},
|
||||
}],
|
||||
loop_variables: [{
|
||||
id: 'loop-variable-1',
|
||||
label: 'Loop Value',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.variable,
|
||||
value: ['start-node', 'answer'],
|
||||
}],
|
||||
}),
|
||||
runInputData: {
|
||||
question: 'hello',
|
||||
},
|
||||
runResult: null as unknown as NodeTracing,
|
||||
loopRunResult: [],
|
||||
setRunInputData: vi.fn(),
|
||||
toVarInputs,
|
||||
varSelectorsToVarInputs,
|
||||
}))
|
||||
|
||||
expect(toVarInputs).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ variable: 'start-node.answer' }),
|
||||
])
|
||||
expect(result.current.forms).toHaveLength(1)
|
||||
expect(result.current.forms[0].inputs).toEqual([
|
||||
createInputVar('start-node.answer'),
|
||||
createInputVar('tool-a.result'),
|
||||
createInputVar('start-node.answer'),
|
||||
])
|
||||
expect(result.current.forms[0].values).toEqual({ question: 'hello' })
|
||||
expect(result.current.allVarObject).toEqual({
|
||||
'start-node.answer@@@tool-a@@@0': {
|
||||
inSingleRunPassedKey: 'passed_key',
|
||||
},
|
||||
})
|
||||
expect(result.current.getDependentVars()).toEqual([
|
||||
['start-node', 'answer'],
|
||||
['tool-a', 'result'],
|
||||
['start-node', 'answer'],
|
||||
])
|
||||
})
|
||||
|
||||
it('should forward onChange and merge tracing metadata into node info', () => {
|
||||
const setRunInputData = vi.fn()
|
||||
const runResult = createRunTrace()
|
||||
|
||||
const { result } = renderHook(() => useSingleRunFormParams({
|
||||
id: 'loop-node',
|
||||
payload: createLoopNode(),
|
||||
runInputData: {},
|
||||
runResult,
|
||||
loopRunResult: [runResult],
|
||||
setRunInputData,
|
||||
toVarInputs: vi.fn(() => []),
|
||||
varSelectorsToVarInputs: vi.fn(() => []),
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.forms[0].onChange({ retry: true })
|
||||
})
|
||||
|
||||
expect(setRunInputData).toHaveBeenCalledWith({ retry: true })
|
||||
expect(mockFormatTracing).toHaveBeenCalledWith([runResult], expect.any(Function))
|
||||
expect(result.current.nodeInfo).toEqual({
|
||||
id: 'formatted-node',
|
||||
execution_metadata: expect.objectContaining({
|
||||
loop_index: 9,
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
171
web/app/components/workflow/nodes/loop/use-config.helpers.ts
Normal file
171
web/app/components/workflow/nodes/loop/use-config.helpers.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import type { ErrorHandleMode, Var } from '../../types'
|
||||
import type { Condition, LoopNodeType, LoopVariable } from './types'
|
||||
import { produce } from 'immer'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { ValueType, VarType } from '../../types'
|
||||
import { LogicalOperator } from './types'
|
||||
import { getOperators } from './utils'
|
||||
|
||||
export const canUseAsLoopInput = (variable: Var) => {
|
||||
return [
|
||||
VarType.array,
|
||||
VarType.arrayString,
|
||||
VarType.arrayNumber,
|
||||
VarType.arrayObject,
|
||||
VarType.arrayFile,
|
||||
].includes(variable.type)
|
||||
}
|
||||
|
||||
export const updateErrorHandleMode = (
|
||||
inputs: LoopNodeType,
|
||||
mode: ErrorHandleMode,
|
||||
) => produce(inputs, (draft) => {
|
||||
draft.error_handle_mode = mode
|
||||
})
|
||||
|
||||
export const addBreakCondition = ({
|
||||
inputs,
|
||||
valueSelector,
|
||||
variable,
|
||||
isVarFileAttribute,
|
||||
}: {
|
||||
inputs: LoopNodeType
|
||||
valueSelector: string[]
|
||||
variable: { type: VarType }
|
||||
isVarFileAttribute: boolean
|
||||
}) => produce(inputs, (draft) => {
|
||||
if (!draft.break_conditions)
|
||||
draft.break_conditions = []
|
||||
|
||||
draft.break_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 ? 'false' : '',
|
||||
})
|
||||
})
|
||||
|
||||
export const removeBreakCondition = (
|
||||
inputs: LoopNodeType,
|
||||
conditionId: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
draft.break_conditions = draft.break_conditions?.filter(item => item.id !== conditionId)
|
||||
})
|
||||
|
||||
export const updateBreakCondition = (
|
||||
inputs: LoopNodeType,
|
||||
conditionId: string,
|
||||
condition: Condition,
|
||||
) => produce(inputs, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition)
|
||||
Object.assign(targetCondition, condition)
|
||||
})
|
||||
|
||||
export const toggleConditionOperator = (inputs: LoopNodeType) => produce(inputs, (draft) => {
|
||||
draft.logical_operator = draft.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
|
||||
export const addSubVariableCondition = (
|
||||
inputs: LoopNodeType,
|
||||
conditionId: string,
|
||||
key?: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
const condition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
|
||||
if (!condition.sub_variable_condition) {
|
||||
condition.sub_variable_condition = {
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
}
|
||||
}
|
||||
|
||||
const comparisonOperators = getOperators(VarType.string, { key: key || '' })
|
||||
condition.sub_variable_condition.conditions.push({
|
||||
id: uuid4(),
|
||||
key: key || '',
|
||||
varType: VarType.string,
|
||||
comparison_operator: comparisonOperators[0],
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
|
||||
export const removeSubVariableCondition = (
|
||||
inputs: LoopNodeType,
|
||||
conditionId: string,
|
||||
subConditionId: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
const condition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (!condition?.sub_variable_condition)
|
||||
return
|
||||
|
||||
condition.sub_variable_condition.conditions = condition.sub_variable_condition.conditions
|
||||
.filter(item => item.id !== subConditionId)
|
||||
})
|
||||
|
||||
export const updateSubVariableCondition = (
|
||||
inputs: LoopNodeType,
|
||||
conditionId: string,
|
||||
subConditionId: string,
|
||||
condition: Condition,
|
||||
) => produce(inputs, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
const targetSubCondition = targetCondition?.sub_variable_condition?.conditions.find(item => item.id === subConditionId)
|
||||
if (targetSubCondition)
|
||||
Object.assign(targetSubCondition, condition)
|
||||
})
|
||||
|
||||
export const toggleSubVariableConditionOperator = (
|
||||
inputs: LoopNodeType,
|
||||
conditionId: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition?.sub_variable_condition) {
|
||||
targetCondition.sub_variable_condition.logical_operator
|
||||
= targetCondition.sub_variable_condition.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
}
|
||||
})
|
||||
|
||||
export const updateLoopCount = (
|
||||
inputs: LoopNodeType,
|
||||
value: number,
|
||||
) => produce(inputs, (draft) => {
|
||||
draft.loop_count = value
|
||||
})
|
||||
|
||||
export const addLoopVariable = (inputs: LoopNodeType) => produce(inputs, (draft) => {
|
||||
if (!draft.loop_variables)
|
||||
draft.loop_variables = []
|
||||
|
||||
draft.loop_variables.push({
|
||||
id: uuid4(),
|
||||
label: '',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.constant,
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
|
||||
export const removeLoopVariable = (
|
||||
inputs: LoopNodeType,
|
||||
id: string,
|
||||
) => produce(inputs, (draft) => {
|
||||
draft.loop_variables = draft.loop_variables?.filter(item => item.id !== id)
|
||||
})
|
||||
|
||||
export const updateLoopVariable = (
|
||||
inputs: LoopNodeType,
|
||||
id: string,
|
||||
updateData: Partial<LoopVariable>,
|
||||
) => produce(inputs, (draft) => {
|
||||
const index = draft.loop_variables?.findIndex(item => item.id === id) ?? -1
|
||||
if (index > -1) {
|
||||
draft.loop_variables![index] = {
|
||||
...draft.loop_variables![index],
|
||||
...updateData,
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -9,13 +9,11 @@ import type {
|
||||
HandleUpdateSubVariableCondition,
|
||||
LoopNodeType,
|
||||
} from './types'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
@ -28,12 +26,25 @@ import {
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { ValueType, VarType } from '../../types'
|
||||
import { toNodeOutputVars } from '../_base/components/variable/utils'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { LogicalOperator } from './types'
|
||||
import {
|
||||
addBreakCondition,
|
||||
addLoopVariable,
|
||||
addSubVariableCondition,
|
||||
canUseAsLoopInput,
|
||||
removeBreakCondition,
|
||||
removeLoopVariable,
|
||||
removeSubVariableCondition,
|
||||
toggleConditionOperator,
|
||||
toggleSubVariableConditionOperator,
|
||||
updateBreakCondition,
|
||||
updateErrorHandleMode,
|
||||
updateLoopCount,
|
||||
updateLoopVariable,
|
||||
updateSubVariableCondition,
|
||||
} from './use-config.helpers'
|
||||
import useIsVarFileAttribute from './use-is-var-file-attribute'
|
||||
import { getOperators } from './utils'
|
||||
|
||||
const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
@ -50,9 +61,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return ([VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile] as VarType[]).includes(varPayload.type)
|
||||
}, [])
|
||||
const filterInputVar = useCallback((varPayload: Var) => canUseAsLoopInput(varPayload), [])
|
||||
|
||||
// output
|
||||
const { getLoopNodeChildren } = useWorkflow()
|
||||
@ -78,158 +87,60 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
})
|
||||
|
||||
const changeErrorResponseMode = useCallback((item: { value: unknown }) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.error_handle_mode = item.value as ErrorHandleMode
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [inputs, handleInputsChange])
|
||||
handleInputsChange(updateErrorHandleMode(inputsRef.current, item.value as ErrorHandleMode))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleAddCondition = useCallback<HandleAddCondition>((valueSelector, varItem) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (!draft.break_conditions)
|
||||
draft.break_conditions = []
|
||||
|
||||
draft.break_conditions?.push({
|
||||
id: uuid4(),
|
||||
varType: varItem.type,
|
||||
variable_selector: valueSelector,
|
||||
comparison_operator: getOperators(varItem.type, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
|
||||
value: varItem.type === VarType.boolean ? 'false' : '',
|
||||
})
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(addBreakCondition({
|
||||
inputs: inputsRef.current,
|
||||
valueSelector,
|
||||
variable: varItem,
|
||||
isVarFileAttribute: !!getIsVarFileAttribute(valueSelector),
|
||||
}))
|
||||
}, [getIsVarFileAttribute, handleInputsChange])
|
||||
|
||||
const handleRemoveCondition = useCallback<HandleRemoveCondition>((conditionId) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.break_conditions = draft.break_conditions?.filter(item => item.id !== conditionId)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(removeBreakCondition(inputsRef.current, conditionId))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateCondition = useCallback<HandleUpdateCondition>((conditionId, newCondition) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition)
|
||||
Object.assign(targetCondition, newCondition)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(updateBreakCondition(inputsRef.current, conditionId, newCondition))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>(() => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.logical_operator = draft.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(toggleConditionOperator(inputsRef.current))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleAddSubVariableCondition = useCallback<HandleAddSubVariableCondition>((conditionId: string, key?: string) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
const condition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
if (!condition?.sub_variable_condition) {
|
||||
condition.sub_variable_condition = {
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
}
|
||||
}
|
||||
const subVarCondition = condition.sub_variable_condition
|
||||
if (subVarCondition) {
|
||||
if (!subVarCondition.conditions)
|
||||
subVarCondition.conditions = []
|
||||
|
||||
const svcComparisonOperators = getOperators(VarType.string, { key: key || '' })
|
||||
|
||||
subVarCondition.conditions.push({
|
||||
id: uuid4(),
|
||||
key: key || '',
|
||||
varType: VarType.string,
|
||||
comparison_operator: (svcComparisonOperators && svcComparisonOperators.length) ? svcComparisonOperators[0] : undefined,
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(addSubVariableCondition(inputsRef.current, conditionId, key))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleRemoveSubVariableCondition = useCallback((conditionId: string, subConditionId: string) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
const condition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
if (!condition?.sub_variable_condition)
|
||||
return
|
||||
const subVarCondition = condition.sub_variable_condition
|
||||
if (subVarCondition)
|
||||
subVarCondition.conditions = subVarCondition.conditions.filter(item => item.id !== subConditionId)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(removeSubVariableCondition(inputsRef.current, conditionId, subConditionId))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateSubVariableCondition = useCallback<HandleUpdateSubVariableCondition>((conditionId, subConditionId, newSubCondition) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition && targetCondition.sub_variable_condition) {
|
||||
const targetSubCondition = targetCondition.sub_variable_condition.conditions.find(item => item.id === subConditionId)
|
||||
if (targetSubCondition)
|
||||
Object.assign(targetSubCondition, newSubCondition)
|
||||
}
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(updateSubVariableCondition(inputsRef.current, conditionId, subConditionId, newSubCondition))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleToggleSubVariableConditionLogicalOperator = useCallback<HandleToggleSubVariableConditionLogicalOperator>((conditionId) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
const targetCondition = draft.break_conditions?.find(item => item.id === conditionId)
|
||||
if (targetCondition && targetCondition.sub_variable_condition)
|
||||
targetCondition.sub_variable_condition.logical_operator = targetCondition.sub_variable_condition.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(toggleSubVariableConditionOperator(inputsRef.current, conditionId))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateLoopCount = useCallback((value: number) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.loop_count = value
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(updateLoopCount(inputsRef.current, value))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleAddLoopVariable = useCallback(() => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (!draft.loop_variables)
|
||||
draft.loop_variables = []
|
||||
|
||||
draft.loop_variables.push({
|
||||
id: uuid4(),
|
||||
label: '',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.constant,
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(addLoopVariable(inputsRef.current))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleRemoveLoopVariable = useCallback((id: string) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.loop_variables = draft.loop_variables?.filter(item => item.id !== id)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(removeLoopVariable(inputsRef.current, id))
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateLoopVariable = useCallback((id: string, updateData: any) => {
|
||||
const loopVariables = inputsRef.current.loop_variables || []
|
||||
const index = loopVariables.findIndex(item => item.id === id)
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (index > -1) {
|
||||
draft.loop_variables![index] = {
|
||||
...draft.loop_variables![index],
|
||||
...updateData,
|
||||
}
|
||||
}
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
handleInputsChange(updateLoopVariable(inputsRef.current, id, updateData))
|
||||
}, [handleInputsChange])
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import {
|
||||
LOOP_CHILDREN_Z_INDEX,
|
||||
LOOP_PADDING,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../loop-start/constants'
|
||||
|
||||
type ContainerBounds = {
|
||||
rightNode?: Node
|
||||
bottomNode?: Node
|
||||
}
|
||||
|
||||
export const getContainerBounds = (childrenNodes: Node[]): ContainerBounds => {
|
||||
return childrenNodes.reduce<ContainerBounds>((acc, node) => {
|
||||
const nextRightNode = !acc.rightNode || node.position.x + node.width! > acc.rightNode.position.x + acc.rightNode.width!
|
||||
? node
|
||||
: acc.rightNode
|
||||
const nextBottomNode = !acc.bottomNode || node.position.y + node.height! > acc.bottomNode.position.y + acc.bottomNode.height!
|
||||
? node
|
||||
: acc.bottomNode
|
||||
|
||||
return {
|
||||
rightNode: nextRightNode,
|
||||
bottomNode: nextBottomNode,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const getContainerResize = (currentNode: Node, bounds: ContainerBounds) => {
|
||||
const width = bounds.rightNode && currentNode.width! < bounds.rightNode.position.x + bounds.rightNode.width!
|
||||
? bounds.rightNode.position.x + bounds.rightNode.width! + LOOP_PADDING.right
|
||||
: undefined
|
||||
const height = bounds.bottomNode && currentNode.height! < bounds.bottomNode.position.y + bounds.bottomNode.height!
|
||||
? bounds.bottomNode.position.y + bounds.bottomNode.height! + LOOP_PADDING.bottom
|
||||
: undefined
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
export const getRestrictedLoopPosition = (node: Node, parentNode?: Node) => {
|
||||
const restrictPosition: { x?: number, y?: number } = { x: undefined, y: undefined }
|
||||
|
||||
if (!node.data.isInLoop || !parentNode)
|
||||
return restrictPosition
|
||||
|
||||
if (node.position.y < LOOP_PADDING.top)
|
||||
restrictPosition.y = LOOP_PADDING.top
|
||||
if (node.position.x < LOOP_PADDING.left)
|
||||
restrictPosition.x = LOOP_PADDING.left
|
||||
if (node.position.x + node.width! > parentNode.width! - LOOP_PADDING.right)
|
||||
restrictPosition.x = parentNode.width! - LOOP_PADDING.right - node.width!
|
||||
if (node.position.y + node.height! > parentNode.height! - LOOP_PADDING.bottom)
|
||||
restrictPosition.y = parentNode.height! - LOOP_PADDING.bottom - node.height!
|
||||
|
||||
return restrictPosition
|
||||
}
|
||||
|
||||
export const getLoopChildren = (nodes: Node[], nodeId: string) => {
|
||||
return nodes.filter(node => node.parentId === nodeId && node.type !== CUSTOM_LOOP_START_NODE)
|
||||
}
|
||||
|
||||
export const buildLoopChildCopy = ({
|
||||
child,
|
||||
childNodeType,
|
||||
defaultValue,
|
||||
nodesWithSameTypeCount,
|
||||
newNodeId,
|
||||
index,
|
||||
}: {
|
||||
child: Node
|
||||
childNodeType: BlockEnum
|
||||
defaultValue: Node['data']
|
||||
nodesWithSameTypeCount: number
|
||||
newNodeId: string
|
||||
index: number
|
||||
}) => {
|
||||
const params = {
|
||||
type: child.type!,
|
||||
data: {
|
||||
...defaultValue,
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
_dimmed: false,
|
||||
title: nodesWithSameTypeCount > 0 ? `${defaultValue.title} ${nodesWithSameTypeCount + 1}` : defaultValue.title,
|
||||
isInLoop: true,
|
||||
loop_id: newNodeId,
|
||||
type: childNodeType,
|
||||
},
|
||||
position: child.position,
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNodeId,
|
||||
extent: child.extent,
|
||||
zIndex: LOOP_CHILDREN_Z_INDEX,
|
||||
}
|
||||
|
||||
return {
|
||||
params,
|
||||
newId: `${newNodeId}${index}`,
|
||||
}
|
||||
}
|
||||
@ -6,15 +6,17 @@ import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useNodesMetaData } from '@/app/components/workflow/hooks'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
import {
|
||||
LOOP_CHILDREN_Z_INDEX,
|
||||
LOOP_PADDING,
|
||||
} from '../../constants'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../../utils'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../loop-start/constants'
|
||||
import {
|
||||
buildLoopChildCopy,
|
||||
getContainerBounds,
|
||||
getContainerResize,
|
||||
getLoopChildren,
|
||||
getRestrictedLoopPosition,
|
||||
} from './use-interactions.helpers'
|
||||
|
||||
export const useNodeLoopInteractions = () => {
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
@ -23,43 +25,20 @@ export const useNodeLoopInteractions = () => {
|
||||
const handleNodeLoopRerender = useCallback((nodeId: string) => {
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_LOOP_START_NODE)
|
||||
if (!childrenNodes.length)
|
||||
return
|
||||
let rightNode: Node
|
||||
let bottomNode: Node
|
||||
const childrenNodes = getLoopChildren(nodes, nodeId)
|
||||
const resize = getContainerResize(currentNode, getContainerBounds(childrenNodes))
|
||||
|
||||
childrenNodes.forEach((n) => {
|
||||
if (rightNode) {
|
||||
if (n.position.x + n.width! > rightNode.position.x + rightNode.width!)
|
||||
rightNode = n
|
||||
}
|
||||
else {
|
||||
rightNode = n
|
||||
}
|
||||
if (bottomNode) {
|
||||
if (n.position.y + n.height! > bottomNode.position.y + bottomNode.height!)
|
||||
bottomNode = n
|
||||
}
|
||||
else {
|
||||
bottomNode = n
|
||||
}
|
||||
})
|
||||
|
||||
const widthShouldExtend = rightNode! && currentNode.width! < rightNode.position.x + rightNode.width!
|
||||
const heightShouldExtend = bottomNode! && currentNode.height! < bottomNode.position.y + bottomNode.height!
|
||||
|
||||
if (widthShouldExtend || heightShouldExtend) {
|
||||
if (resize.width || resize.height) {
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((n) => {
|
||||
if (n.id === nodeId) {
|
||||
if (widthShouldExtend) {
|
||||
n.data.width = rightNode.position.x + rightNode.width! + LOOP_PADDING.right
|
||||
n.width = rightNode.position.x + rightNode.width! + LOOP_PADDING.right
|
||||
if (resize.width) {
|
||||
n.data.width = resize.width
|
||||
n.width = resize.width
|
||||
}
|
||||
if (heightShouldExtend) {
|
||||
n.data.height = bottomNode.position.y + bottomNode.height! + LOOP_PADDING.bottom
|
||||
n.height = bottomNode.position.y + bottomNode.height! + LOOP_PADDING.bottom
|
||||
if (resize.height) {
|
||||
n.data.height = resize.height
|
||||
n.height = resize.height
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -72,25 +51,8 @@ export const useNodeLoopInteractions = () => {
|
||||
const handleNodeLoopChildDrag = useCallback((node: Node) => {
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
|
||||
const restrictPosition: { x?: number, y?: number } = { x: undefined, y: undefined }
|
||||
|
||||
if (node.data.isInLoop) {
|
||||
const parentNode = nodes.find(n => n.id === node.parentId)
|
||||
|
||||
if (parentNode) {
|
||||
if (node.position.y < LOOP_PADDING.top)
|
||||
restrictPosition.y = LOOP_PADDING.top
|
||||
if (node.position.x < LOOP_PADDING.left)
|
||||
restrictPosition.x = LOOP_PADDING.left
|
||||
if (node.position.x + node.width! > parentNode!.width! - LOOP_PADDING.right)
|
||||
restrictPosition.x = parentNode!.width! - LOOP_PADDING.right - node.width!
|
||||
if (node.position.y + node.height! > parentNode!.height! - LOOP_PADDING.bottom)
|
||||
restrictPosition.y = parentNode!.height! - LOOP_PADDING.bottom - node.height!
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrictPosition,
|
||||
restrictPosition: getRestrictedLoopPosition(node, nodes.find(n => n.id === node.parentId)),
|
||||
}
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
@ -105,35 +67,26 @@ export const useNodeLoopInteractions = () => {
|
||||
|
||||
const handleNodeLoopChildrenCopy = useCallback((nodeId: string, newNodeId: string, idMapping: Record<string, string>) => {
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_LOOP_START_NODE)
|
||||
const childrenNodes = getLoopChildren(nodes, nodeId)
|
||||
const newIdMapping = { ...idMapping }
|
||||
|
||||
const copyChildren = childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const defaultValue = nodesMetaDataMap?.[childNodeType]?.defaultValue ?? {}
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...defaultValue,
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
_dimmed: false,
|
||||
title: nodesWithSameType.length > 0 ? `${defaultValue.title} ${nodesWithSameType.length + 1}` : defaultValue.title,
|
||||
isInLoop: true,
|
||||
loop_id: newNodeId,
|
||||
type: childNodeType,
|
||||
},
|
||||
position: child.position,
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNodeId,
|
||||
extent: child.extent,
|
||||
zIndex: LOOP_CHILDREN_Z_INDEX,
|
||||
const childCopy = buildLoopChildCopy({
|
||||
child,
|
||||
childNodeType,
|
||||
defaultValue: defaultValue as Node['data'],
|
||||
nodesWithSameTypeCount: nodesWithSameType.length,
|
||||
newNodeId,
|
||||
index,
|
||||
})
|
||||
newNode.id = `${newNodeId}${newNode.id + index}`
|
||||
const { newNode } = generateNewNode({
|
||||
...childCopy.params,
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
})
|
||||
newNode.id = `${newNodeId}${newNode.id + childCopy.newId}`
|
||||
newIdMapping[child.id] = newNode.id
|
||||
return newNode
|
||||
})
|
||||
|
||||
@ -0,0 +1,131 @@
|
||||
import type { InputVar, Node, ValueSelector, Variable } from '../../types'
|
||||
import type { CaseItem, Condition, LoopVariable } from './types'
|
||||
import { ValueType } from '@/app/components/workflow/types'
|
||||
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar } from '../_base/components/variable/utils'
|
||||
|
||||
export function getVarSelectorsFromCase(caseItem: CaseItem): ValueSelector[] {
|
||||
const vars: ValueSelector[] = []
|
||||
caseItem.conditions?.forEach((condition) => {
|
||||
vars.push(...getVarSelectorsFromCondition(condition))
|
||||
})
|
||||
return vars
|
||||
}
|
||||
|
||||
export function getVarSelectorsFromCondition(condition: Condition): ValueSelector[] {
|
||||
const vars: ValueSelector[] = []
|
||||
if (condition.variable_selector)
|
||||
vars.push(condition.variable_selector)
|
||||
|
||||
if (condition.sub_variable_condition?.conditions?.length)
|
||||
vars.push(...getVarSelectorsFromCase(condition.sub_variable_condition))
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
export const createInputVarValues = (runInputData: Record<string, unknown>) => {
|
||||
const vars: Record<string, unknown> = {}
|
||||
Object.keys(runInputData).forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
}
|
||||
|
||||
export const dedupeInputVars = (inputVars: InputVar[]) => {
|
||||
const seen: Record<string, boolean> = {}
|
||||
const uniqueInputVars: InputVar[] = []
|
||||
|
||||
inputVars.forEach((input) => {
|
||||
if (!input || seen[input.variable])
|
||||
return
|
||||
|
||||
seen[input.variable] = true
|
||||
uniqueInputVars.push(input)
|
||||
})
|
||||
|
||||
return uniqueInputVars
|
||||
}
|
||||
|
||||
export const buildUsedOutVars = ({
|
||||
loopChildrenNodes,
|
||||
currentNodeId,
|
||||
canChooseVarNodes,
|
||||
isNodeInLoop,
|
||||
toVarInputs,
|
||||
}: {
|
||||
loopChildrenNodes: Node[]
|
||||
currentNodeId: string
|
||||
canChooseVarNodes: Node[]
|
||||
isNodeInLoop: (nodeId: string) => boolean
|
||||
toVarInputs: (variables: Variable[]) => InputVar[]
|
||||
}) => {
|
||||
const vars: ValueSelector[] = []
|
||||
const seenVarSelectors: Record<string, boolean> = {}
|
||||
const allVarObject: Record<string, { inSingleRunPassedKey: string }> = {}
|
||||
|
||||
loopChildrenNodes.forEach((node) => {
|
||||
const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0)
|
||||
nodeVars.forEach((varSelector) => {
|
||||
if (varSelector[0] === currentNodeId)
|
||||
return
|
||||
if (isNodeInLoop(varSelector[0]))
|
||||
return
|
||||
|
||||
const varSelectorStr = varSelector.join('.')
|
||||
if (!seenVarSelectors[varSelectorStr]) {
|
||||
seenVarSelectors[varSelectorStr] = true
|
||||
vars.push(varSelector)
|
||||
}
|
||||
|
||||
let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector)
|
||||
if (typeof passToServerKeys === 'string')
|
||||
passToServerKeys = [passToServerKeys]
|
||||
|
||||
passToServerKeys.forEach((key: string, index: number) => {
|
||||
allVarObject[[varSelectorStr, node.id, index].join(DELIMITER)] = {
|
||||
inSingleRunPassedKey: key,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const usedOutVars = toVarInputs(vars.map((valueSelector) => {
|
||||
const varInfo = getNodeInfoById(canChooseVarNodes, valueSelector[0])
|
||||
return {
|
||||
label: {
|
||||
nodeType: varInfo?.data.type,
|
||||
nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title,
|
||||
variable: isSystemVar(valueSelector) ? valueSelector.join('.') : valueSelector[valueSelector.length - 1],
|
||||
},
|
||||
variable: valueSelector.join('.'),
|
||||
value_selector: valueSelector,
|
||||
}
|
||||
}))
|
||||
|
||||
return { usedOutVars, allVarObject }
|
||||
}
|
||||
|
||||
export const getDependentVarsFromLoopPayload = ({
|
||||
nodeId,
|
||||
usedOutVars,
|
||||
breakConditions,
|
||||
loopVariables,
|
||||
}: {
|
||||
nodeId: string
|
||||
usedOutVars: InputVar[]
|
||||
breakConditions?: Condition[]
|
||||
loopVariables?: LoopVariable[]
|
||||
}) => {
|
||||
const vars: ValueSelector[] = usedOutVars.map(item => item.variable.split('.'))
|
||||
|
||||
breakConditions?.forEach((condition) => {
|
||||
vars.push(...getVarSelectorsFromCondition(condition))
|
||||
})
|
||||
|
||||
loopVariables?.forEach((loopVariable) => {
|
||||
if (loopVariable.value_type === ValueType.variable)
|
||||
vars.push(loopVariable.value)
|
||||
})
|
||||
|
||||
return vars.filter(item => item[0] !== nodeId)
|
||||
}
|
||||
@ -1,15 +1,19 @@
|
||||
import type { InputVar, Node, ValueSelector, Variable } from '../../types'
|
||||
import type { CaseItem, Condition, LoopNodeType } from './types'
|
||||
import type { LoopNodeType } from './types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback, useContext, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||
import formatTracing from '@/app/components/workflow/run/utils/format-log'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { ValueType } from '@/app/components/workflow/types'
|
||||
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
|
||||
import { useIsNodeInLoop, useWorkflow } from '../../hooks'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar } from '../_base/components/variable/utils'
|
||||
import {
|
||||
buildUsedOutVars,
|
||||
createInputVarValues,
|
||||
dedupeInputVars,
|
||||
getDependentVarsFromLoopPayload,
|
||||
getVarSelectorsFromCondition,
|
||||
} from './use-single-run-form-params.helpers'
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
@ -37,9 +41,10 @@ const useSingleRunFormParams = ({
|
||||
const { isNodeInLoop } = useIsNodeInLoop(id)
|
||||
|
||||
const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const parentAvailableNodes = useStore(useShallow(s => s.parentAvailableNodes)) || []
|
||||
const workflowStore = useContext(WorkflowContext)
|
||||
const parentAvailableNodes = workflowStore?.getState().parentAvailableNodes || []
|
||||
const loopChildrenNodes = getLoopNodeChildren(id)
|
||||
const beforeNodes = (() => {
|
||||
const beforeNodes = useMemo(() => {
|
||||
const baseBeforeNodes = getBeforeNodesInSameBranch(id)
|
||||
if (!parentAvailableNodes.length)
|
||||
return baseBeforeNodes
|
||||
@ -52,59 +57,16 @@ const useSingleRunFormParams = ({
|
||||
merged.set(node.id, node)
|
||||
})
|
||||
return Array.from(merged.values())
|
||||
})()
|
||||
const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes]
|
||||
}, [getBeforeNodesInSameBranch, id, parentAvailableNodes])
|
||||
const canChooseVarNodes = useMemo(() => [...beforeNodes, ...loopChildrenNodes], [beforeNodes, loopChildrenNodes])
|
||||
|
||||
const { usedOutVars, allVarObject } = (() => {
|
||||
const vars: ValueSelector[] = []
|
||||
const varObjs: Record<string, boolean> = {}
|
||||
const allVarObject: Record<string, {
|
||||
inSingleRunPassedKey: string
|
||||
}> = {}
|
||||
loopChildrenNodes.forEach((node) => {
|
||||
const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0)
|
||||
nodeVars.forEach((varSelector) => {
|
||||
if (varSelector[0] === id) { // skip loop node itself variable: item, index
|
||||
return
|
||||
}
|
||||
const isInLoop = isNodeInLoop(varSelector[0])
|
||||
if (isInLoop) // not pass loop inner variable
|
||||
return
|
||||
|
||||
const varSectorStr = varSelector.join('.')
|
||||
if (!varObjs[varSectorStr]) {
|
||||
varObjs[varSectorStr] = true
|
||||
vars.push(varSelector)
|
||||
}
|
||||
let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector)
|
||||
if (typeof passToServerKeys === 'string')
|
||||
passToServerKeys = [passToServerKeys]
|
||||
|
||||
passToServerKeys.forEach((key: string, index: number) => {
|
||||
allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = {
|
||||
inSingleRunPassedKey: key,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const res = toVarInputs(vars.map((item) => {
|
||||
const varInfo = getNodeInfoById(canChooseVarNodes, item[0])
|
||||
return {
|
||||
label: {
|
||||
nodeType: varInfo?.data.type,
|
||||
nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title
|
||||
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
|
||||
},
|
||||
variable: `${item.join('.')}`,
|
||||
value_selector: item,
|
||||
}
|
||||
}))
|
||||
return {
|
||||
usedOutVars: res,
|
||||
allVarObject,
|
||||
}
|
||||
})()
|
||||
const { usedOutVars, allVarObject } = useMemo(() => buildUsedOutVars({
|
||||
loopChildrenNodes,
|
||||
currentNodeId: id,
|
||||
canChooseVarNodes,
|
||||
isNodeInLoop,
|
||||
toVarInputs,
|
||||
}), [loopChildrenNodes, id, canChooseVarNodes, isNodeInLoop, toVarInputs])
|
||||
|
||||
const nodeInfo = useMemo(() => {
|
||||
const formattedNodeInfo = formatTracing(loopRunResult, t)[0]
|
||||
@ -126,38 +88,9 @@ const useSingleRunFormParams = ({
|
||||
setRunInputData(newPayload)
|
||||
}, [setRunInputData])
|
||||
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
const inputVarValues = useMemo(() => createInputVarValues(runInputData), [runInputData])
|
||||
|
||||
const getVarSelectorsFromCase = (caseItem: CaseItem): ValueSelector[] => {
|
||||
const vars: ValueSelector[] = []
|
||||
if (caseItem.conditions && caseItem.conditions.length) {
|
||||
caseItem.conditions.forEach((condition) => {
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const conditionVars = getVarSelectorsFromCondition(condition)
|
||||
vars.push(...conditionVars)
|
||||
})
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
const getVarSelectorsFromCondition = (condition: Condition) => {
|
||||
const vars: ValueSelector[] = []
|
||||
if (condition.variable_selector)
|
||||
vars.push(condition.variable_selector)
|
||||
|
||||
if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length)
|
||||
vars.push(...getVarSelectorsFromCase(condition.sub_variable_condition))
|
||||
return vars
|
||||
}
|
||||
|
||||
const forms = (() => {
|
||||
const forms = useMemo(() => {
|
||||
const allInputs: ValueSelector[] = []
|
||||
payload.break_conditions?.forEach((condition) => {
|
||||
const vars = getVarSelectorsFromCondition(condition)
|
||||
@ -170,16 +103,7 @@ const useSingleRunFormParams = ({
|
||||
})
|
||||
const inputVarsFromValue: InputVar[] = []
|
||||
const varInputs = [...varSelectorsToVarInputs(allInputs), ...inputVarsFromValue]
|
||||
const existVarsKey: Record<string, boolean> = {}
|
||||
const uniqueVarInputs: InputVar[] = []
|
||||
varInputs.forEach((input) => {
|
||||
if (!input)
|
||||
return
|
||||
if (!existVarsKey[input.variable]) {
|
||||
existVarsKey[input.variable] = true
|
||||
uniqueVarInputs.push(input)
|
||||
}
|
||||
})
|
||||
const uniqueVarInputs = dedupeInputVars(varInputs)
|
||||
return [
|
||||
{
|
||||
inputs: [...usedOutVars, ...uniqueVarInputs],
|
||||
@ -187,43 +111,14 @@ const useSingleRunFormParams = ({
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
]
|
||||
})()
|
||||
}, [payload.break_conditions, payload.loop_variables, varSelectorsToVarInputs, usedOutVars, inputVarValues, setInputVarValues])
|
||||
|
||||
const getVarFromCaseItem = (caseItem: CaseItem): ValueSelector[] => {
|
||||
const vars: ValueSelector[] = []
|
||||
if (caseItem.conditions && caseItem.conditions.length) {
|
||||
caseItem.conditions.forEach((condition) => {
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const conditionVars = getVarFromCondition(condition)
|
||||
vars.push(...conditionVars)
|
||||
})
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
const getVarFromCondition = (condition: Condition): ValueSelector[] => {
|
||||
const vars: ValueSelector[] = []
|
||||
if (condition.variable_selector)
|
||||
vars.push(condition.variable_selector)
|
||||
|
||||
if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length)
|
||||
vars.push(...getVarFromCaseItem(condition.sub_variable_condition))
|
||||
return vars
|
||||
}
|
||||
|
||||
const getDependentVars = () => {
|
||||
const vars: ValueSelector[] = usedOutVars.map(item => item.variable.split('.'))
|
||||
payload.break_conditions?.forEach((condition) => {
|
||||
const conditionVars = getVarFromCondition(condition)
|
||||
vars.push(...conditionVars)
|
||||
})
|
||||
payload.loop_variables?.forEach((loopVariable) => {
|
||||
if (loopVariable.value_type === ValueType.variable)
|
||||
vars.push(loopVariable.value)
|
||||
})
|
||||
const hasFilterLoopVars = vars.filter(item => item[0] !== id)
|
||||
return hasFilterLoopVars
|
||||
}
|
||||
const getDependentVars = useCallback(() => getDependentVarsFromLoopPayload({
|
||||
nodeId: id,
|
||||
usedOutVars,
|
||||
breakConditions: payload.break_conditions,
|
||||
loopVariables: payload.loop_variables,
|
||||
}), [id, usedOutVars, payload.break_conditions, payload.loop_variables])
|
||||
|
||||
return {
|
||||
forms,
|
||||
|
||||
Reference in New Issue
Block a user