chore: (webhook) use variable instead of InputVar (#25119)

This commit is contained in:
非法操作 2025-09-04 11:10:42 +08:00 committed by GitHub
parent 5cf3d24018
commit cc84a45244
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 67 additions and 238 deletions

View File

@ -296,25 +296,15 @@ const formatItem = (
variables = [],
} = data as WebhookTriggerNodeType
res.vars = variables.map((v) => {
const type = inputVarTypeToVarType(v.type)
const type = v.value_type || VarType.string
const varRes: Var = {
variable: v.variable,
type,
isParagraph: v.type === InputVarType.paragraph,
isSelect: v.type === InputVarType.select,
isParagraph: false,
isSelect: false,
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
})

View File

@ -4,8 +4,9 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import GenericTable from './generic-table'
import type { ColumnConfig, GenericTableRow } from './generic-table'
import type { ParameterType, WebhookParameter } from '../types'
import type { WebhookParameter } from '../types'
import { createParameterTypeOptions, normalizeParameterType } from '../utils/parameter-type-utils'
import { VarType } from '@/app/components/workflow/types'
type ParameterTableProps = {
title: string
@ -60,7 +61,7 @@ const ParameterTable: FC<ParameterTableProps> = ({
]
// Choose sensible default type for new rows according to content type
const defaultTypeValue: ParameterType = typeOptions[0]?.value || 'string'
const defaultTypeValue: VarType = typeOptions[0]?.value || 'string'
// Empty row template for new rows
const emptyRowData: GenericTableRow = {
@ -83,7 +84,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' : normalizeParameterType((row.type as string) || 'string'),
type: isTextPlain ? VarType.string : normalizeParameterType((row.type as string)),
required: Boolean(row.required),
}))

View File

@ -1,15 +1,9 @@
import type { CommonNodeType, InputVar } from '@/app/components/workflow/types'
import type { CommonNodeType, VarType, Variable } from '@/app/components/workflow/types'
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'
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'
@ -17,7 +11,7 @@ export const getArrayElementType = (arrayType: `array[${ArrayElementType}]`): Ar
export type WebhookParameter = {
name: string
type: ParameterType
type: VarType
required: boolean
}
@ -37,5 +31,5 @@ export type WebhookTriggerNodeType = CommonNodeType & {
async_mode: boolean
status_code: number
response_body: string
variables: InputVar[]
variables: Variable[]
}

View File

@ -1,15 +1,14 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import type { HttpMethod, ParameterType, WebhookHeader, WebhookParameter, WebhookTriggerNodeType } from './types'
import { getArrayElementType, isArrayType } from './types'
import type { HttpMethod, WebhookHeader, WebhookParameter, WebhookTriggerNodeType } from './types'
import { useNodesReadOnly, useWorkflow } 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'
import { fetchWebhookUrl } from '@/service/apps'
import type { InputVar } from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types'
import type { Variable } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import Toast from '@/app/components/base/toast'
import { hasDuplicateStr } from '@/utils/var'
@ -32,36 +31,6 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
}))
}, [inputs, setInputs])
// Helper function to convert ParameterType to 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,
object: InputVarType.jsonObject,
file: InputVarType.singleFile,
}
return typeMap[type] || InputVarType.textInput
}, [])
const syncVariablesInDraft = useCallback((
draft: WebhookTriggerNodeType,
newData: (WebhookParameter | WebhookHeader)[],
@ -105,13 +74,14 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
const existingVarIndex = draft.variables.findIndex(v => v.variable === varName)
const inputVarType = 'type' in item
? toInputVarType(item.type)
: InputVarType.textInput
? item.type
: VarType.string // Default to string for headers
const newVar: InputVar = {
type: inputVarType,
const newVar: Variable = {
value_type: inputVarType,
label: sourceType, // Use sourceType as label to identify source
variable: varName,
value_selector: [],
required: item.required,
}
@ -122,7 +92,7 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
})
return true
}, [toInputVarType, t, id, isVarUsedInNodes, removeUsedVarInNodes])
}, [t, id, isVarUsedInNodes, removeUsedVarInNodes])
const handleParamsChange = useCallback((params: WebhookParameter[]) => {
setInputs(produce(inputs, (draft) => {

View File

@ -1,10 +1,8 @@
import {
createParameterTypeOptions,
getAvailableParameterTypes,
getParameterTypeDisplayName,
isValidParameterType,
normalizeParameterType,
validateParameterValue,
} from './parameter-type-utils'
describe('Parameter Type Utils', () => {
@ -46,72 +44,6 @@ describe('Parameter Type Utils', () => {
})
})
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)

View File

@ -1,48 +1,53 @@
import type { ArrayElementType, ParameterType } from '../types'
import { VarType } from '@/app/components/workflow/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
const BASIC_TYPES = [VarType.string, VarType.number, VarType.boolean, VarType.object, VarType.file] as const
const ARRAY_ELEMENT_TYPES = [VarType.arrayString, VarType.arrayNumber, VarType.arrayBoolean, VarType.arrayObject] as const
// Generate all valid parameter types programmatically
const VALID_PARAMETER_TYPES: readonly ParameterType[] = [
const VALID_PARAMETER_TYPES: readonly VarType[] = [
...BASIC_TYPES,
...ARRAY_ELEMENT_TYPES.map(type => `array[${type}]` as const),
...ARRAY_ELEMENT_TYPES,
] 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]',
const TYPE_DISPLAY_NAMES: Record<VarType, string> = {
[VarType.string]: 'String',
[VarType.number]: 'Number',
[VarType.boolean]: 'Boolean',
[VarType.object]: 'Object',
[VarType.file]: 'File',
[VarType.arrayString]: 'Array[String]',
[VarType.arrayNumber]: 'Array[Number]',
[VarType.arrayBoolean]: 'Array[Boolean]',
[VarType.arrayObject]: 'Array[Object]',
[VarType.secret]: 'Secret',
[VarType.array]: 'Array',
'array[file]': 'Array[File]',
[VarType.any]: 'Any',
'array[any]': 'Array[Any]',
} 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)],
supportedTypes: [...BASIC_TYPES.filter(t => t !== 'file'), ...ARRAY_ELEMENT_TYPES],
description: 'JSON supports all types including arrays',
},
'text/plain': {
supportedTypes: ['string'] as const,
supportedTypes: [VarType.string] as const,
description: 'Plain text only supports string',
},
'application/x-www-form-urlencoded': {
supportedTypes: ['string', 'number', 'boolean'] as const,
supportedTypes: [VarType.string, VarType.number, VarType.boolean] as const,
description: 'Form data supports basic types',
},
'forms': {
supportedTypes: ['string', 'number', 'boolean'] as const,
supportedTypes: [VarType.string, VarType.number, VarType.boolean] as const,
description: 'Form data supports basic types',
},
'multipart/form-data': {
supportedTypes: ['string', 'number', 'boolean', 'file'] as const,
supportedTypes: [VarType.string, VarType.number, VarType.boolean, VarType.file] as const,
description: 'Multipart supports basic types plus files',
},
} as const
@ -50,113 +55,50 @@ const CONTENT_TYPE_CONFIGS = {
/**
* Type guard to check if a string is a valid parameter type
*/
export const isValidParameterType = (type: string): type is ParameterType => {
export const isValidParameterType = (type: string): type is VarType => {
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 => {
export const normalizeParameterType = (input: string | undefined | null): VarType => {
if (!input || typeof input !== 'string')
return 'string'
return VarType.string
const trimmed = input.trim().toLowerCase()
if (trimmed === 'array[string]')
return VarType.arrayString
else if (trimmed === 'array[number]')
return VarType.arrayNumber
else if (trimmed === 'array[boolean]')
return VarType.arrayBoolean
else if (trimmed === 'array[object]')
return VarType.arrayObject
else if (trimmed === 'number')
return VarType.number
else if (trimmed === 'boolean')
return VarType.boolean
else if (trimmed === 'object')
return VarType.object
else if (trimmed === 'file')
return VarType.file
// 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'
return VarType.string
}
/**
* Gets display name for parameter types in UI components
*/
export const getParameterTypeDisplayName = (type: ParameterType): string => {
export const getParameterTypeDisplayName = (type: VarType): 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[] => {
export const getAvailableParameterTypes = (contentType?: string, isRequestBody = false): VarType[] => {
if (!isRequestBody) {
// Query parameters and headers are always strings
return ['string']
return [VarType.string]
}
const normalizedContentType = (contentType || '').toLowerCase()