mirror of
https://github.com/langgenius/dify.git
synced 2026-02-04 01:51:23 +08:00
refactor: unify trigger node architecture and clean up technical debt (#24886)
Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6d307cc9fc
commit
327b354cc2
@ -48,7 +48,7 @@ class WebhookData(BaseNodeData):
|
||||
SYNC = "async" # only support
|
||||
|
||||
method: Method = Method.GET
|
||||
content_type: ContentType = Field(alias="content-type", default=ContentType.JSON)
|
||||
content_type: ContentType = Field(default=ContentType.JSON)
|
||||
headers: Sequence[WebhookParameter] = Field(default_factory=list)
|
||||
params: Sequence[WebhookParameter] = Field(default_factory=list) # query parameters
|
||||
body: Sequence[WebhookBodyParameter] = Field(default_factory=list)
|
||||
|
||||
@ -42,7 +42,7 @@ class TriggerWebhookNode(BaseNode):
|
||||
"type": "webhook",
|
||||
"config": {
|
||||
"method": "get",
|
||||
"content-type": "application/json",
|
||||
"content_type": "application/json",
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": [],
|
||||
|
||||
@ -89,7 +89,7 @@ class TestWebhookService:
|
||||
"data": {
|
||||
"title": "Test Webhook",
|
||||
"method": "post",
|
||||
"content-type": "application/json",
|
||||
"content_type": "application/json",
|
||||
"headers": [
|
||||
{"name": "Authorization", "required": True},
|
||||
{"name": "Content-Type", "required": False},
|
||||
|
||||
@ -508,7 +508,7 @@ export const RETRIEVAL_OUTPUT_STRUCT = `{
|
||||
}`
|
||||
|
||||
export const SUPPORT_OUTPUT_VARS_NODE = [
|
||||
BlockEnum.Start, BlockEnum.LLM, BlockEnum.KnowledgeRetrieval, BlockEnum.Code, BlockEnum.TemplateTransform,
|
||||
BlockEnum.Start, BlockEnum.TriggerWebhook, BlockEnum.LLM, BlockEnum.KnowledgeRetrieval, BlockEnum.Code, BlockEnum.TemplateTransform,
|
||||
BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier,
|
||||
BlockEnum.ParameterExtractor, BlockEnum.Iteration, BlockEnum.Loop,
|
||||
BlockEnum.DocExtractor, BlockEnum.ListFilter,
|
||||
|
||||
@ -22,6 +22,7 @@ import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
||||
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { WebhookTriggerNodeType } from '@/app/components/workflow/nodes/trigger-webhook/types'
|
||||
|
||||
import {
|
||||
AGENT_OUTPUT_STRUCT,
|
||||
@ -290,6 +291,36 @@ const formatItem = (
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.TriggerWebhook: {
|
||||
const {
|
||||
variables = [],
|
||||
} = data as WebhookTriggerNodeType
|
||||
res.vars = variables.map((v) => {
|
||||
const type = inputVarTypeToVarType(v.type)
|
||||
const varRes: Var = {
|
||||
variable: v.variable,
|
||||
type,
|
||||
isParagraph: v.type === InputVarType.paragraph,
|
||||
isSelect: v.type === InputVarType.select,
|
||||
options: v.options,
|
||||
required: v.required,
|
||||
}
|
||||
try {
|
||||
if(type === VarType.object && v.json_schema) {
|
||||
varRes.children = {
|
||||
schema: JSON.parse(v.json_schema),
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error formatting TriggerWebhook variable:', error)
|
||||
}
|
||||
return varRes
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
res.vars = [...LLM_OUTPUT_STRUCT]
|
||||
if (data.structured_output_enabled && data.structured_output?.schema?.properties && Object.keys(data.structured_output.schema.properties).length > 0) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { PluginTriggerNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
|
||||
const nodeDefault: NodeDefault<PluginTriggerNodeType> = {
|
||||
defaultValue: {
|
||||
@ -15,8 +15,8 @@ const nodeDefault: NodeDefault<PluginTriggerNodeType> = {
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
? []
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes.filter(type => type !== BlockEnum.Start)
|
||||
},
|
||||
checkValid(payload: PluginTriggerNodeType, t: any) {
|
||||
|
||||
@ -32,7 +32,6 @@ describe('Schedule Trigger Node Default', () => {
|
||||
it('should have correct default value', () => {
|
||||
expect(nodeDefault.defaultValue.mode).toBe('visual')
|
||||
expect(nodeDefault.defaultValue.frequency).toBe('weekly')
|
||||
expect(nodeDefault.defaultValue.enabled).toBe(true)
|
||||
})
|
||||
|
||||
it('should have empty prev nodes', () => {
|
||||
|
||||
@ -9,7 +9,6 @@ const createMonthlyConfig = (monthly_days: (number | 'last')[], time = '10:30 AM
|
||||
monthly_days,
|
||||
},
|
||||
timezone,
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
describe('Monthly Edge Cases', () => {
|
||||
|
||||
@ -9,7 +9,6 @@ const createMonthlyConfig = (monthlyDays: (number | 'last')[], time = '10:30 AM'
|
||||
monthly_days: monthlyDays,
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
id: 'test',
|
||||
type: 'trigger-schedule',
|
||||
data: {},
|
||||
@ -117,8 +116,7 @@ describe('Monthly Multi-Select Execution Time Calculator', () => {
|
||||
time: '10:30 AM',
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
id: 'test',
|
||||
id: 'test',
|
||||
type: 'trigger-schedule',
|
||||
data: {},
|
||||
position: { x: 0, y: 0 },
|
||||
|
||||
@ -22,7 +22,6 @@ describe('Monthly Validation', () => {
|
||||
monthly_days: [15],
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
@ -39,7 +38,6 @@ describe('Monthly Validation', () => {
|
||||
monthly_days: ['last' as const],
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
@ -58,7 +56,6 @@ describe('Monthly Validation', () => {
|
||||
monthly_days: [1, 15, 30],
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
@ -75,7 +72,6 @@ describe('Monthly Validation', () => {
|
||||
monthly_days: [1, 15, 'last' as const],
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
@ -92,7 +88,6 @@ describe('Monthly Validation', () => {
|
||||
monthly_days: [],
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
@ -109,7 +104,6 @@ describe('Monthly Validation', () => {
|
||||
monthly_days: [1, 35, 15],
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
@ -127,7 +121,6 @@ describe('Monthly Validation', () => {
|
||||
time: '10:30 AM',
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
@ -144,7 +137,6 @@ describe('Monthly Validation', () => {
|
||||
monthly_days: [1, 15],
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
@ -161,7 +153,6 @@ describe('Monthly Validation', () => {
|
||||
monthly_days: Array.from({ length: 31 }, (_, i) => i + 1),
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const result = nodeDefault.checkValid(config, mockT)
|
||||
|
||||
@ -15,7 +15,6 @@ const createWeeklyConfig = (
|
||||
weekdays,
|
||||
},
|
||||
timezone,
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
describe('Weekly Schedule Time Logic Tests', () => {
|
||||
@ -364,8 +363,7 @@ describe('Weekly Schedule Time Logic Tests', () => {
|
||||
time: '2:00 PM',
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
const weeklyTimes = getNextExecutionTimes(weeklyConfig, 1)
|
||||
const dailyTimes = getNextExecutionTimes(dailyConfig, 1)
|
||||
@ -389,8 +387,7 @@ describe('Weekly Schedule Time Logic Tests', () => {
|
||||
time: '2:00 PM',
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
const weeklyTimes = getNextExecutionTimes(weeklyConfig, 1)
|
||||
const dailyTimes = getNextExecutionTimes(dailyConfig, 1)
|
||||
|
||||
@ -4,7 +4,6 @@ import type { ScheduleTriggerNodeType } from './types'
|
||||
export const getDefaultScheduleConfig = (): Partial<ScheduleTriggerNodeType> => ({
|
||||
mode: 'visual',
|
||||
frequency: 'weekly',
|
||||
enabled: true,
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
weekdays: ['sun'],
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { ScheduleTriggerNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { isValidCronExpression } from './utils/cron-parser'
|
||||
import { getNextExecutionTimes } from './utils/execution-time-calculator'
|
||||
import { getDefaultScheduleConfig } from './constants'
|
||||
@ -114,8 +114,8 @@ const nodeDefault: NodeDefault<ScheduleTriggerNodeType> = {
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
? []
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes.filter(type => type !== BlockEnum.Start)
|
||||
},
|
||||
checkValid(payload: ScheduleTriggerNodeType, t: any) {
|
||||
|
||||
@ -17,5 +17,4 @@ export type ScheduleTriggerNodeType = CommonNodeType & {
|
||||
cron_expression?: string // Cron expression when mode is 'cron'
|
||||
visual_config?: VisualConfig // User-friendly configuration when mode is 'visual'
|
||||
timezone: string // User profile timezone (e.g., 'Asia/Shanghai', 'America/New_York')
|
||||
enabled: boolean // Whether the trigger is active
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ const useConfig = (id: string, payload: ScheduleTriggerNodeType) => {
|
||||
mode: payload.mode || 'visual',
|
||||
frequency: payload.frequency || 'weekly',
|
||||
timezone: userProfile.timezone || 'UTC',
|
||||
enabled: payload.enabled !== undefined ? payload.enabled : true,
|
||||
visual_config: {
|
||||
...getDefaultVisualConfig(),
|
||||
...payload.visual_config,
|
||||
|
||||
@ -14,7 +14,6 @@ const createMockData = (overrides: Partial<ScheduleTriggerNodeType> = {}): Sched
|
||||
time: '2:30 PM',
|
||||
},
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ describe('Webhook Trigger Node Default', () => {
|
||||
// Core webhook configuration
|
||||
expect(defaultValue.webhook_url).toBe('')
|
||||
expect(defaultValue.method).toBe('POST')
|
||||
expect(defaultValue['content-type']).toBe('application/json')
|
||||
expect(defaultValue.content_type).toBe('application/json')
|
||||
|
||||
// Response configuration fields
|
||||
expect(defaultValue.async_mode).toBe(true)
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { WebhookTriggerNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import type { DefaultValueForm } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
|
||||
const nodeDefault: NodeDefault<WebhookTriggerNodeType> = {
|
||||
defaultValue: {
|
||||
'webhook_url': '',
|
||||
'method': 'POST',
|
||||
'content-type': 'application/json',
|
||||
'headers': [],
|
||||
'params': [],
|
||||
'body': [],
|
||||
'async_mode': true,
|
||||
'status_code': 200,
|
||||
'response_body': '',
|
||||
'default_value': [] as DefaultValueForm[],
|
||||
webhook_url: '',
|
||||
method: 'POST',
|
||||
content_type: 'application/json',
|
||||
headers: [],
|
||||
params: [],
|
||||
body: [],
|
||||
async_mode: true,
|
||||
status_code: 200,
|
||||
response_body: '',
|
||||
default_value: [] as DefaultValueForm[],
|
||||
},
|
||||
getAvailablePrevNodes(_isChatMode: boolean) {
|
||||
return []
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
? []
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes.filter(type => type !== BlockEnum.Start)
|
||||
},
|
||||
checkValid(_payload: WebhookTriggerNodeType, _t: any) {
|
||||
|
||||
@ -135,7 +135,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
<div className="w-full">
|
||||
<SimpleSelect
|
||||
items={CONTENT_TYPES}
|
||||
defaultValue={inputs['content-type']}
|
||||
defaultValue={inputs.content_type}
|
||||
onSelect={item => handleContentTypeChange(item.value as string)}
|
||||
disabled={readOnly}
|
||||
className="h-8 text-sm"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import type { CommonNodeType, InputVar } from '@/app/components/workflow/types'
|
||||
import type { DefaultValueForm } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
|
||||
@ -18,17 +18,18 @@ export type WebhookHeader = {
|
||||
}
|
||||
|
||||
export type WebhookTriggerNodeType = CommonNodeType & {
|
||||
'webhook_url'?: string
|
||||
'webhook_debug_url'?: string
|
||||
'method': HttpMethod
|
||||
'content-type': string
|
||||
'headers': WebhookHeader[]
|
||||
'params': WebhookParameter[]
|
||||
'body': WebhookParameter[]
|
||||
'async_mode': boolean
|
||||
'status_code': number
|
||||
'response_body': string
|
||||
'http_methods'?: HttpMethod[]
|
||||
'error_strategy'?: ErrorHandleTypeEnum
|
||||
'default_value'?: DefaultValueForm[]
|
||||
webhook_url?: string
|
||||
webhook_debug_url?: string
|
||||
method: HttpMethod
|
||||
content_type: string
|
||||
headers: WebhookHeader[]
|
||||
params: WebhookParameter[]
|
||||
body: WebhookParameter[]
|
||||
async_mode: boolean
|
||||
status_code: number
|
||||
response_body: string
|
||||
http_methods?: HttpMethod[]
|
||||
error_strategy?: ErrorHandleTypeEnum
|
||||
default_value?: DefaultValueForm[]
|
||||
variables: InputVar[]
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { HttpMethod, WebhookHeader, WebhookParameter, WebhookTriggerNodeType } from './types'
|
||||
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
@ -7,8 +8,13 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import type { DefaultValueForm } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import { fetchWebhookUrl } from '@/service/apps'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { hasDuplicateStr } from '@/utils/var'
|
||||
|
||||
const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
|
||||
const { t } = useTranslation()
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<WebhookTriggerNodeType>(id, payload)
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
@ -21,27 +27,84 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
|
||||
|
||||
const handleContentTypeChange = useCallback((contentType: string) => {
|
||||
setInputs(produce(inputs, (draft) => {
|
||||
draft['content-type'] = contentType
|
||||
draft.content_type = contentType
|
||||
}))
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleHeadersChange = useCallback((headers: WebhookHeader[]) => {
|
||||
setInputs(produce(inputs, (draft) => {
|
||||
draft.headers = headers
|
||||
}))
|
||||
}, [inputs, setInputs])
|
||||
// Helper function to convert ParameterType to InputVarType
|
||||
const toInputVarType = useCallback((type: string): InputVarType => {
|
||||
const typeMap: Record<string, InputVarType> = {
|
||||
string: InputVarType.textInput,
|
||||
number: InputVarType.number,
|
||||
boolean: InputVarType.checkbox,
|
||||
array: InputVarType.textInput, // Arrays as text for now
|
||||
object: InputVarType.jsonObject,
|
||||
}
|
||||
return typeMap[type] || InputVarType.textInput
|
||||
}, [])
|
||||
|
||||
const syncVariablesInDraft = useCallback((
|
||||
draft: WebhookTriggerNodeType,
|
||||
newData: (WebhookParameter | WebhookHeader)[],
|
||||
) => {
|
||||
if (!draft.variables)
|
||||
draft.variables = []
|
||||
|
||||
if(hasDuplicateStr(newData.map(item => item.name))) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('appDebug.varKeyError.keyAlreadyExists', {
|
||||
key: t('appDebug.variableConfig.varName'),
|
||||
}),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// Add or update variables
|
||||
newData.forEach((item) => {
|
||||
const varName = item.name
|
||||
const existingVarIndex = draft.variables.findIndex(v => v.variable === varName)
|
||||
|
||||
const inputVarType = 'type' in item
|
||||
? toInputVarType(item.type)
|
||||
: InputVarType.textInput // Headers default to text
|
||||
|
||||
const newVar: InputVar = {
|
||||
type: inputVarType,
|
||||
label: varName,
|
||||
variable: varName,
|
||||
required: item.required,
|
||||
}
|
||||
|
||||
if (existingVarIndex >= 0)
|
||||
draft.variables[existingVarIndex] = newVar
|
||||
else
|
||||
draft.variables.push(newVar)
|
||||
})
|
||||
|
||||
return true
|
||||
}, [toInputVarType, t])
|
||||
|
||||
const handleParamsChange = useCallback((params: WebhookParameter[]) => {
|
||||
setInputs(produce(inputs, (draft) => {
|
||||
draft.params = params
|
||||
syncVariablesInDraft(draft, params)
|
||||
}))
|
||||
}, [inputs, setInputs])
|
||||
}, [inputs, setInputs, syncVariablesInDraft])
|
||||
|
||||
const handleHeadersChange = useCallback((headers: WebhookHeader[]) => {
|
||||
setInputs(produce(inputs, (draft) => {
|
||||
draft.headers = headers
|
||||
syncVariablesInDraft(draft, headers)
|
||||
}))
|
||||
}, [inputs, setInputs, syncVariablesInDraft])
|
||||
|
||||
const handleBodyChange = useCallback((body: WebhookParameter[]) => {
|
||||
setInputs(produce(inputs, (draft) => {
|
||||
draft.body = body
|
||||
syncVariablesInDraft(draft, body)
|
||||
}))
|
||||
}, [inputs, setInputs])
|
||||
}, [inputs, setInputs, syncVariablesInDraft])
|
||||
|
||||
const handleAsyncModeChange = useCallback((asyncMode: boolean) => {
|
||||
setInputs(produce(inputs, (draft) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user