fix(webhook-trigger): request array type adjustment (#25005)

This commit is contained in:
cathy 2025-09-02 23:20:12 +08:00 committed by GitHub
parent 1d1bb9451e
commit d522350c99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 426 additions and 105 deletions

View File

@ -28,7 +28,7 @@ const getTriggerIcon = (trigger: AppTrigger) => {
<div className="absolute -left-0.5 -top-0.5 h-1.5 w-1.5 rounded-sm border border-black/15 bg-green-500" />
)
}
else {
else {
return (
<div className="absolute -left-0.5 -top-0.5 h-1.5 w-1.5 rounded-sm border border-components-badge-status-light-disabled-border-inner bg-components-badge-status-light-disabled-bg shadow-status-indicator-gray-shadow" />
)
@ -96,7 +96,7 @@ function TriggerCard({ appInfo }: ITriggerCardProps) {
})
invalidateAppTriggers(appId)
}
catch (error) {
catch (error) {
console.error('Failed to update trigger status:', error)
}
}

View File

@ -61,7 +61,7 @@ const RunMode = memo(() => {
if (option.type === 'user_input') {
handleWorkflowStartRunInWorkflow()
}
else {
else {
// Placeholder for trigger-specific execution logic for schedule, webhook, plugin types
console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId)
}

View File

@ -102,7 +102,7 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
/>
)
}
else if (toolIcon && typeof toolIcon === 'object' && 'content' in toolIcon) {
else if (toolIcon && typeof toolIcon === 'object' && 'content' in toolIcon) {
icon = (
<AppIcon
className="!h-6 !w-6 rounded-lg border-[0.5px] border-white/2 shadow-md"
@ -112,7 +112,7 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
/>
)
}
else {
else {
icon = (
<div className="bg-util-colors-white-white-500 flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 text-white shadow-md">
<span className="text-xs font-medium text-text-tertiary">P</span>

View File

@ -114,7 +114,7 @@ export const useNodesInteractions = () => {
try {
return checkNestedParallelLimit(nodes, edges, parentNodeId)
}
catch (error: any) {
catch (error: any) {
if (handleStartNodeMissingError(error, operationKey))
return false // Operation blocked but gracefully handled

View File

@ -38,7 +38,7 @@ const Field: FC<Props> = ({
<div className={cn(className, inline && 'flex w-full items-center justify-between')}>
<div
onClick={() => supportFold && toggleFold()}
className={cn('sticky top-0 z-10 flex items-center justify-between bg-components-panel-bg', supportFold && 'cursor-pointer')}>
className={cn('flex items-center justify-between', supportFold && 'cursor-pointer')}>
<div className='flex h-6 items-center'>
<div className={cn(isSubTitle ? 'system-xs-medium-uppercase text-text-tertiary' : 'system-sm-semibold-uppercase text-text-secondary')}>
{title} {required && <span className='text-text-destructive'>*</span>}

View File

@ -116,7 +116,7 @@ describe('Monthly Multi-Select Execution Time Calculator', () => {
time: '10:30 AM',
},
timezone: 'UTC',
id: 'test',
id: 'test',
type: 'trigger-schedule',
data: {},
position: { x: 0, y: 0 },

View File

@ -363,7 +363,7 @@ describe('Weekly Schedule Time Logic Tests', () => {
time: '2:00 PM',
},
timezone: 'UTC',
}
}
const weeklyTimes = getNextExecutionTimes(weeklyConfig, 1)
const dailyTimes = getNextExecutionTimes(dailyConfig, 1)
@ -387,7 +387,7 @@ describe('Weekly Schedule Time Logic Tests', () => {
time: '2:00 PM',
},
timezone: 'UTC',
}
}
const weeklyTimes = getNextExecutionTimes(weeklyConfig, 1)
const dailyTimes = getNextExecutionTimes(dailyConfig, 1)

View File

@ -130,7 +130,7 @@ const nodeDefault: NodeDefault<ScheduleTriggerNodeType> = {
try {
Intl.DateTimeFormat(undefined, { timeZone: payload.timezone })
}
catch {
catch {
errorMessages = t('workflow.nodes.triggerSchedule.invalidTimezone')
}
}
@ -138,13 +138,13 @@ const nodeDefault: NodeDefault<ScheduleTriggerNodeType> = {
if (payload.mode === 'cron') {
if (!payload.cron_expression || payload.cron_expression.trim() === '')
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.cronExpression') })
else if (!isValidCronExpression(payload.cron_expression))
else if (!isValidCronExpression(payload.cron_expression))
errorMessages = t('workflow.nodes.triggerSchedule.invalidCronExpression')
}
else if (payload.mode === 'visual') {
else if (payload.mode === 'visual') {
if (!payload.frequency)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.frequency') })
else
else
errorMessages = validateVisualConfig(payload, t)
}
}
@ -154,7 +154,7 @@ const nodeDefault: NodeDefault<ScheduleTriggerNodeType> = {
if (nextTimes.length === 0)
errorMessages = t('workflow.nodes.triggerSchedule.noValidExecutionTime')
}
catch {
catch {
errorMessages = t('workflow.nodes.triggerSchedule.executionTimeCalculationError')
}
}

View File

@ -12,7 +12,7 @@ const matchesField = (value: number, pattern: string, min: number, max: number):
if (range === '*') {
return value % stepValue === min % stepValue
}
else {
else {
const rangeStart = Number.parseInt(range, 10)
if (Number.isNaN(rangeStart)) return false
return value >= rangeStart && (value - rangeStart) % stepValue === 0
@ -96,15 +96,15 @@ const matchesCron = (
return matchesField(currentDay, dayOfMonth, 1, 31)
|| matchesField(currentDayOfWeek, dayOfWeek, 0, 6)
}
else if (dayOfMonthSpecified) {
else if (dayOfMonthSpecified) {
// Only day of month specified
return matchesField(currentDay, dayOfMonth, 1, 31)
}
else if (dayOfWeekSpecified) {
else if (dayOfWeekSpecified) {
// Only day of week specified
return matchesField(currentDayOfWeek, dayOfWeek, 0, 6)
}
else {
else {
// Both are *, matches any day
return true
}

View File

@ -5,20 +5,7 @@ import { useTranslation } from 'react-i18next'
import GenericTable from './generic-table'
import type { ColumnConfig, GenericTableRow } from './generic-table'
import type { ParameterType, WebhookParameter } from '../types'
const normalizeParamType = (type: string): ParameterType => {
switch (type) {
case 'string':
case 'number':
case 'boolean':
case 'array':
case 'object':
case 'file':
return type
default:
return 'string'
}
}
import { createParameterTypeOptions, normalizeParameterType } from '../utils/parameter-type-utils'
type ParameterTableProps = {
title: string
@ -43,59 +30,7 @@ const ParameterTable: FC<ParameterTableProps> = ({
}) => {
const { t } = useTranslation()
const resolveTypeOptions = (): { name: string; value: ParameterType }[] => {
if (!isRequestBody) {
return [
{ name: 'String', value: 'string' },
]
}
const ct = (contentType || '').toLowerCase()
// application/json -> all JSON-friendly types
if (ct === 'application/json') {
return [
{ name: 'String', value: 'string' },
{ name: 'Number', value: 'number' },
{ name: 'Boolean', value: 'boolean' },
{ name: 'Array', value: 'array' },
{ name: 'Object', value: 'object' },
]
}
// text/plain -> plain text only
if (ct === 'text/plain') {
return [
{ name: 'String', value: 'string' },
]
}
// x-www-form-urlencoded and forms -> simple key-value pairs
if (ct === 'application/x-www-form-urlencoded' || ct === 'forms') {
return [
{ name: 'String', value: 'string' },
{ name: 'Number', value: 'number' },
{ name: 'Boolean', value: 'boolean' },
]
}
// multipart/form-data -> allow file additionally
if (ct === 'multipart/form-data') {
return [
{ name: 'String', value: 'string' },
{ name: 'Number', value: 'number' },
{ name: 'Boolean', value: 'boolean' },
{ name: 'File', value: 'file' },
]
}
// Fallback: all json-like types
return [
{ name: 'String', value: 'string' },
{ name: 'Number', value: 'number' },
{ name: 'Boolean', value: 'boolean' },
{ name: 'Array', value: 'array' },
{ name: 'Object', value: 'object' },
]
}
const typeOptions = resolveTypeOptions()
const typeOptions = createParameterTypeOptions(contentType, isRequestBody)
// Define columns based on component type - matching prototype design
const columns: ColumnConfig[] = [
@ -125,10 +60,7 @@ const ParameterTable: FC<ParameterTableProps> = ({
]
// Choose sensible default type for new rows according to content type
const defaultTypeValue = ((): ParameterType => {
const first = typeOptions[0]?.value
return first || 'string'
})()
const defaultTypeValue: ParameterType = typeOptions[0]?.value || 'string'
// Empty row template for new rows
const emptyRowData: GenericTableRow = {
@ -151,7 +83,7 @@ const ParameterTable: FC<ParameterTableProps> = ({
.filter(row => typeof row.key === 'string' && (row.key as string).trim() !== '')
.map(row => ({
name: String(row.key),
type: isTextPlain ? 'string' : normalizeParamType((row.type as string) || 'string'),
type: isTextPlain ? 'string' : normalizeParameterType((row.type as string) || 'string'),
required: Boolean(row.required),
}))

View File

@ -1,6 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { WebhookTriggerNodeType } from './types'
import { isValidParameterType } from './utils/parameter-type-utils'
import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<WebhookTriggerNodeType> = {
@ -24,7 +25,34 @@ const nodeDefault: NodeDefault<WebhookTriggerNodeType> = {
: ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes.filter(type => type !== BlockEnum.Start)
},
checkValid(_payload: WebhookTriggerNodeType, _t: any) {
checkValid(payload: WebhookTriggerNodeType, t: any) {
// Validate webhook configuration
if (!payload.webhook_url) {
return {
isValid: false,
errorMessage: t('workflow.nodes.webhook.validation.webhookUrlRequired'),
}
}
// Validate parameter types for params and body
const parametersWithTypes = [
...(payload.params || []),
...(payload.body || []),
]
for (const param of parametersWithTypes) {
// Validate parameter type is valid
if (!isValidParameterType(param.type)) {
return {
isValid: false,
errorMessage: t('workflow.nodes.webhook.validation.invalidParameterType', {
name: param.name,
type: param.type,
}),
}
}
}
return {
isValid: true,
errorMessage: '',

View File

@ -173,7 +173,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
placeholder={t(`${i18nPrefix}.noBodyParameters`)}
showType={true}
isRequestBody={true}
contentType={inputs['content-type']}
contentType={inputs.content_type}
/>
<Split />

View File

@ -2,7 +2,18 @@ import type { CommonNodeType, InputVar } from '@/app/components/workflow/types'
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'
export type ParameterType = 'string' | 'number' | 'boolean' | 'array' | 'object' | 'file'
export type ParameterType = 'string' | 'number' | 'boolean' | 'array[string]' | 'array[number]' | 'array[boolean]' | 'array[object]' | 'object' | 'file'
export type ArrayElementType = 'string' | 'number' | 'boolean' | 'object'
export const isArrayType = (type: ParameterType): type is `array[${ArrayElementType}]` => {
return type.startsWith('array[') && type.endsWith(']')
}
export const getArrayElementType = (arrayType: `array[${ArrayElementType}]`): ArrayElementType => {
const match = arrayType.match(/^array\[(.+)\]$/)
return (match?.[1] as ArrayElementType) || 'string'
}
export type WebhookParameter = {
name: string

View File

@ -1,7 +1,9 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import type { HttpMethod, WebhookHeader, WebhookParameter, WebhookTriggerNodeType } from './types'
import type { HttpMethod, ParameterType, WebhookHeader, WebhookParameter, WebhookTriggerNodeType } from './types'
import { getArrayElementType, isArrayType } from './types'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { useStore as useAppStore } from '@/app/components/app/store'
@ -30,13 +32,31 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
}, [inputs, setInputs])
// Helper function to convert ParameterType to InputVarType
const toInputVarType = useCallback((type: string): InputVarType => {
const toInputVarType = useCallback((type: ParameterType): InputVarType => {
// Handle specific array types
if (isArrayType(type)) {
const elementType = getArrayElementType(type)
switch (elementType) {
case 'string':
return InputVarType.textInput
case 'number':
return InputVarType.number
case 'boolean':
return InputVarType.checkbox
case 'object':
return InputVarType.jsonObject
default:
return InputVarType.textInput
}
}
// Handle non-array types
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,
file: InputVarType.singleFile,
}
return typeMap[type] || InputVarType.textInput
}, [])
@ -49,12 +69,12 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
draft.variables = []
if(hasDuplicateStr(newData.map(item => item.name))) {
Toast.notify({
type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', {
key: t('appDebug.variableConfig.varName'),
}),
})
Toast.notify({
type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', {
key: t('appDebug.variableConfig.varName'),
}),
})
return false
}
@ -76,7 +96,7 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
if (existingVarIndex >= 0)
draft.variables[existingVarIndex] = newVar
else
else
draft.variables.push(newVar)
})

View File

@ -0,0 +1,149 @@
import {
createParameterTypeOptions,
getAvailableParameterTypes,
getParameterTypeDisplayName,
isValidParameterType,
normalizeParameterType,
validateParameterValue,
} from './parameter-type-utils'
describe('Parameter Type Utils', () => {
describe('isValidParameterType', () => {
it('should validate specific array types', () => {
expect(isValidParameterType('array[string]')).toBe(true)
expect(isValidParameterType('array[number]')).toBe(true)
expect(isValidParameterType('array[boolean]')).toBe(true)
expect(isValidParameterType('array[object]')).toBe(true)
})
it('should validate basic types', () => {
expect(isValidParameterType('string')).toBe(true)
expect(isValidParameterType('number')).toBe(true)
expect(isValidParameterType('boolean')).toBe(true)
expect(isValidParameterType('object')).toBe(true)
expect(isValidParameterType('file')).toBe(true)
})
it('should reject invalid types', () => {
expect(isValidParameterType('array')).toBe(false)
expect(isValidParameterType('invalid')).toBe(false)
expect(isValidParameterType('array[invalid]')).toBe(false)
})
})
describe('normalizeParameterType', () => {
it('should normalize valid types', () => {
expect(normalizeParameterType('string')).toBe('string')
expect(normalizeParameterType('array[string]')).toBe('array[string]')
})
it('should migrate legacy array type', () => {
expect(normalizeParameterType('array')).toBe('array[string]')
})
it('should default to string for invalid types', () => {
expect(normalizeParameterType('invalid')).toBe('string')
})
})
describe('getParameterTypeDisplayName', () => {
it('should return correct display names for array types', () => {
expect(getParameterTypeDisplayName('array[string]')).toBe('Array[String]')
expect(getParameterTypeDisplayName('array[number]')).toBe('Array[Number]')
expect(getParameterTypeDisplayName('array[boolean]')).toBe('Array[Boolean]')
expect(getParameterTypeDisplayName('array[object]')).toBe('Array[Object]')
})
it('should return correct display names for basic types', () => {
expect(getParameterTypeDisplayName('string')).toBe('String')
expect(getParameterTypeDisplayName('number')).toBe('Number')
expect(getParameterTypeDisplayName('boolean')).toBe('Boolean')
expect(getParameterTypeDisplayName('object')).toBe('Object')
expect(getParameterTypeDisplayName('file')).toBe('File')
})
})
describe('validateParameterValue', () => {
it('should validate string values', () => {
expect(validateParameterValue('test', 'string')).toBe(true)
expect(validateParameterValue('', 'string')).toBe(true)
expect(validateParameterValue(123, 'string')).toBe(false)
})
it('should validate number values', () => {
expect(validateParameterValue(123, 'number')).toBe(true)
expect(validateParameterValue(123.45, 'number')).toBe(true)
expect(validateParameterValue('abc', 'number')).toBe(false)
expect(validateParameterValue(Number.NaN, 'number')).toBe(false)
})
it('should validate boolean values', () => {
expect(validateParameterValue(true, 'boolean')).toBe(true)
expect(validateParameterValue(false, 'boolean')).toBe(true)
expect(validateParameterValue('true', 'boolean')).toBe(false)
})
it('should validate array values', () => {
expect(validateParameterValue(['a', 'b'], 'array[string]')).toBe(true)
expect(validateParameterValue([1, 2, 3], 'array[number]')).toBe(true)
expect(validateParameterValue([true, false], 'array[boolean]')).toBe(true)
expect(validateParameterValue([{ key: 'value' }], 'array[object]')).toBe(true)
expect(validateParameterValue(['a', 1], 'array[string]')).toBe(false)
expect(validateParameterValue('not an array', 'array[string]')).toBe(false)
})
it('should validate object values', () => {
expect(validateParameterValue({ key: 'value' }, 'object')).toBe(true)
expect(validateParameterValue({}, 'object')).toBe(true)
expect(validateParameterValue(null, 'object')).toBe(false)
expect(validateParameterValue([], 'object')).toBe(false)
expect(validateParameterValue('string', 'object')).toBe(false)
})
it('should validate file values', () => {
const mockFile = new File(['content'], 'test.txt', { type: 'text/plain' })
expect(validateParameterValue(mockFile, 'file')).toBe(true)
expect(validateParameterValue({ name: 'file.txt' }, 'file')).toBe(true)
expect(validateParameterValue('not a file', 'file')).toBe(false)
})
it('should return false for invalid types', () => {
expect(validateParameterValue('test', 'invalid' as any)).toBe(false)
})
})
describe('getAvailableParameterTypes', () => {
it('should return only string for non-request body', () => {
const types = getAvailableParameterTypes('application/json', false)
expect(types).toEqual(['string'])
})
it('should return all types for application/json', () => {
const types = getAvailableParameterTypes('application/json', true)
expect(types).toContain('string')
expect(types).toContain('number')
expect(types).toContain('boolean')
expect(types).toContain('array[string]')
expect(types).toContain('array[number]')
expect(types).toContain('array[boolean]')
expect(types).toContain('array[object]')
expect(types).toContain('object')
})
it('should include file type for multipart/form-data', () => {
const types = getAvailableParameterTypes('multipart/form-data', true)
expect(types).toContain('file')
})
})
describe('createParameterTypeOptions', () => {
it('should create options with display names', () => {
const options = createParameterTypeOptions('application/json', true)
const stringOption = options.find(opt => opt.value === 'string')
const arrayStringOption = options.find(opt => opt.value === 'array[string]')
expect(stringOption?.name).toBe('String')
expect(arrayStringOption?.name).toBe('Array[String]')
})
})
})

View File

@ -0,0 +1,181 @@
import type { ArrayElementType, ParameterType } from '../types'
// Constants for better maintainability and reusability
const BASIC_TYPES = ['string', 'number', 'boolean', 'object', 'file'] as const
const ARRAY_ELEMENT_TYPES = ['string', 'number', 'boolean', 'object'] as const
// Generate all valid parameter types programmatically
const VALID_PARAMETER_TYPES: readonly ParameterType[] = [
...BASIC_TYPES,
...ARRAY_ELEMENT_TYPES.map(type => `array[${type}]` as const),
] as const
// Type display name mappings
const TYPE_DISPLAY_NAMES: Record<ParameterType, string> = {
'string': 'String',
'number': 'Number',
'boolean': 'Boolean',
'object': 'Object',
'file': 'File',
'array[string]': 'Array[String]',
'array[number]': 'Array[Number]',
'array[boolean]': 'Array[Boolean]',
'array[object]': 'Array[Object]',
} as const
// Content type configurations
const CONTENT_TYPE_CONFIGS = {
'application/json': {
supportedTypes: [...BASIC_TYPES.filter(t => t !== 'file'), ...ARRAY_ELEMENT_TYPES.map(t => `array[${t}]` as const)],
description: 'JSON supports all types including arrays',
},
'text/plain': {
supportedTypes: ['string'] as const,
description: 'Plain text only supports string',
},
'application/x-www-form-urlencoded': {
supportedTypes: ['string', 'number', 'boolean'] as const,
description: 'Form data supports basic types',
},
'forms': {
supportedTypes: ['string', 'number', 'boolean'] as const,
description: 'Form data supports basic types',
},
'multipart/form-data': {
supportedTypes: ['string', 'number', 'boolean', 'file'] as const,
description: 'Multipart supports basic types plus files',
},
} as const
/**
* Type guard to check if a string is a valid parameter type
*/
export const isValidParameterType = (type: string): type is ParameterType => {
return (VALID_PARAMETER_TYPES as readonly string[]).includes(type)
}
/**
* Type-safe helper to check if a string is a valid array element type
*/
const isValidArrayElementType = (type: string): type is ArrayElementType => {
return (ARRAY_ELEMENT_TYPES as readonly string[]).includes(type)
}
/**
* Type-safe helper to check if a string is a valid basic type
*/
const isValidBasicType = (type: string): type is Exclude<ParameterType, `array[${ArrayElementType}]`> => {
return (BASIC_TYPES as readonly string[]).includes(type)
}
/**
* Normalizes parameter type from various input formats to the new type system
* Handles legacy 'array' type and malformed inputs gracefully
*/
export const normalizeParameterType = (input: string | undefined | null): ParameterType => {
if (!input || typeof input !== 'string')
return 'string'
const trimmed = input.trim().toLowerCase()
// Handle legacy array type
if (trimmed === 'array')
return 'array[string]' // Default to string array for backward compatibility
// Handle specific array types
if (trimmed.startsWith('array[') && trimmed.endsWith(']')) {
const elementType = trimmed.slice(6, -1) // Extract content between 'array[' and ']'
if (isValidArrayElementType(elementType))
return `array[${elementType}]`
// Invalid array element type, default to string array
return 'array[string]'
}
// Handle basic types
if (isValidBasicType(trimmed))
return trimmed
// Fallback to string for unknown types
return 'string'
}
/**
* Gets display name for parameter types in UI components
*/
export const getParameterTypeDisplayName = (type: ParameterType): string => {
return TYPE_DISPLAY_NAMES[type]
}
// Type validation functions for better reusability
const validators = {
string: (value: unknown): value is string => typeof value === 'string',
number: (value: unknown): value is number => typeof value === 'number' && !isNaN(value),
boolean: (value: unknown): value is boolean => typeof value === 'boolean',
object: (value: unknown): value is object =>
typeof value === 'object' && value !== null && !Array.isArray(value),
file: (value: unknown): value is File =>
value instanceof File || (typeof value === 'object' && value !== null),
} as const
/**
* Validates array elements based on element type
*/
const validateArrayElements = (value: unknown[], elementType: ArrayElementType): boolean => {
const validator = validators[elementType]
return value.every(item => validator(item))
}
/**
* Validates parameter value against its declared type
* Provides runtime type checking for webhook parameters
*/
export const validateParameterValue = (value: unknown, type: ParameterType): boolean => {
// Handle basic types
if (type in validators) {
const validator = validators[type as keyof typeof validators]
return validator(value)
}
// Handle array types
if (type.startsWith('array[') && type.endsWith(']')) {
if (!Array.isArray(value)) return false
const elementType = type.slice(6, -1)
return isValidArrayElementType(elementType) && validateArrayElements(value, elementType)
}
return false
}
/**
* Gets available parameter types based on content type
* Provides context-aware type filtering for different webhook content types
*/
export const getAvailableParameterTypes = (contentType?: string, isRequestBody = false): ParameterType[] => {
if (!isRequestBody) {
// Query parameters and headers are always strings
return ['string']
}
const normalizedContentType = (contentType || '').toLowerCase()
const configKey = normalizedContentType in CONTENT_TYPE_CONFIGS
? normalizedContentType as keyof typeof CONTENT_TYPE_CONFIGS
: 'application/json'
const config = CONTENT_TYPE_CONFIGS[configKey]
return [...config.supportedTypes]
}
/**
* Creates type options for UI select components
*/
export const createParameterTypeOptions = (contentType?: string, isRequestBody = false) => {
const availableTypes = getAvailableParameterTypes(contentType, isRequestBody)
return availableTypes.map(type => ({
name: getParameterTypeDisplayName(type),
value: type,
}))
}

View File

@ -134,7 +134,7 @@ export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
}
})
}
else {
else {
// Leaf node - add iteration/loop children if any
if (root.data.type === BlockEnum.Iteration)
list.push(...nodes.filter(node => node.parentId === root.id))