fix(webhook): add content-type aware parameter type handling (#24865)

This commit is contained in:
cathy 2025-09-01 10:06:26 +08:00 committed by GitHub
parent 9ed45594c6
commit 10f19cd0c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 87 additions and 14 deletions

View File

@ -8,6 +8,11 @@ import { SimpleSelect } from '@/app/components/base/select'
import { replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
import cn from '@/utils/classnames'
// Tiny utility to judge whether a cell value is effectively present
const isPresent = (v: unknown): boolean => {
if (typeof v === 'string') return v.trim() !== ''
return !(v === '' || v === null || v === undefined || v === false)
}
// Column configuration types for table components
export type ColumnType = 'input' | 'select' | 'switch' | 'custom'
@ -120,6 +125,11 @@ const GenericTable: FC<GenericTableProps> = ({
onChange(next)
}, [data, emptyRowData, onChange, readonly])
// Determine the primary identifier column just once
const primaryKey = useMemo(() => (
columns.find(col => col.key === 'key' || col.key === 'name')?.key ?? 'key'
), [columns])
const renderCell = (column: ColumnConfig, row: GenericTableRow, dataIndex: number | null) => {
const value = row[column.key]
const handleChange = (newValue: unknown) => updateRow(dataIndex, column.key, newValue)
@ -218,8 +228,8 @@ const GenericTable: FC<GenericTableProps> = ({
const rowKey = `row-${renderIndex}`
// Check if primary identifier column has content
const primaryColumn = columns.find(col => col.key === 'key' || col.key === 'name')?.key || 'key'
const hasContent = row[primaryColumn] && String(row[primaryColumn]).trim() !== ''
const primaryValue = row[primaryKey]
const hasContent = isPresent(primaryValue)
return (
<div

View File

@ -13,6 +13,7 @@ const normalizeParamType = (type: string): ParameterType => {
case 'boolean':
case 'array':
case 'object':
case 'file':
return type
default:
return 'string'
@ -27,6 +28,7 @@ type ParameterTableProps = {
placeholder?: string
showType?: boolean
isRequestBody?: boolean // Special handling for request body parameters
contentType?: string
}
const ParameterTable: FC<ParameterTableProps> = ({
@ -37,17 +39,63 @@ const ParameterTable: FC<ParameterTableProps> = ({
placeholder,
showType = true,
isRequestBody = false,
contentType,
}) => {
const { t } = useTranslation()
// Type options for request body parameters
const typeOptions = [
{ name: 'String', value: 'string' },
{ name: 'Number', value: 'number' },
{ name: 'Boolean', value: 'boolean' },
{ name: 'Array', value: 'array' },
{ name: 'Object', value: 'object' },
]
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()
// Define columns based on component type - matching prototype design
const columns: ColumnConfig[] = [
@ -76,10 +124,16 @@ 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'
})()
// Empty row template for new rows
const emptyRowData: GenericTableRow = {
key: '',
type: isRequestBody ? 'string' : '',
type: isRequestBody ? defaultTypeValue : '',
required: false,
}
@ -90,13 +144,21 @@ const ParameterTable: FC<ParameterTableProps> = ({
}))
const handleDataChange = (data: GenericTableRow[]) => {
const newParams: WebhookParameter[] = data
// For text/plain, enforce single text body semantics: keep only first non-empty row and force string type
const isTextPlain = isRequestBody && (contentType || '').toLowerCase() === 'text/plain'
const normalized = data
.filter(row => typeof row.key === 'string' && (row.key as string).trim() !== '')
.map(row => ({
name: String(row.key),
type: normalizeParamType((row.type as string) || 'string'),
type: isTextPlain ? 'string' : normalizeParamType((row.type as string) || 'string'),
required: Boolean(row.required),
}))
const newParams: WebhookParameter[] = isTextPlain
? normalized.slice(0, 1)
: normalized
onChange(newParams)
}

View File

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

View File

@ -4,7 +4,7 @@ import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'
export type ParameterType = 'string' | 'number' | 'boolean' | 'array' | 'object'
export type ParameterType = 'string' | 'number' | 'boolean' | 'array' | 'object' | 'file'
export type WebhookParameter = {
name: string