mirror of https://github.com/langgenius/dify.git
fix(webhook): add content-type aware parameter type handling (#24865)
This commit is contained in:
parent
9ed45594c6
commit
10f19cd0c2
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
|||
placeholder={t(`${i18nPrefix}.noBodyParameters`)}
|
||||
showType={true}
|
||||
isRequestBody={true}
|
||||
contentType={inputs['content-type']}
|
||||
/>
|
||||
|
||||
<Split />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue