From 60b5ed8e5d0228919b12d2f6a5d52260042c8621 Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:41:42 +0800 Subject: [PATCH] fix: enhance webhook trigger panel UI consistency and user experience (#24780) --- .../trigger-webhook/__tests__/default.test.ts | 86 +++++++++++++++++ .../components/generic-table.tsx | 95 ++++++++++--------- .../components/header-table.tsx | 6 +- .../components/paragraph-input.tsx | 57 +++++++++++ .../components/parameter-table.tsx | 6 +- .../workflow/nodes/trigger-webhook/panel.tsx | 37 ++------ web/i18n/en-US/workflow.ts | 4 +- web/i18n/ja-JP/workflow.ts | 4 +- web/i18n/zh-Hans/workflow.ts | 4 +- 9 files changed, 213 insertions(+), 86 deletions(-) create mode 100644 web/app/components/workflow/nodes/trigger-webhook/__tests__/default.test.ts create mode 100644 web/app/components/workflow/nodes/trigger-webhook/components/paragraph-input.tsx diff --git a/web/app/components/workflow/nodes/trigger-webhook/__tests__/default.test.ts b/web/app/components/workflow/nodes/trigger-webhook/__tests__/default.test.ts new file mode 100644 index 0000000000..11a81c4a94 --- /dev/null +++ b/web/app/components/workflow/nodes/trigger-webhook/__tests__/default.test.ts @@ -0,0 +1,86 @@ +/** + * Webhook Trigger Node Default Tests + * + * Tests for webhook trigger node default configuration and field validation. + * Tests core checkValid functionality following project patterns. + */ + +import nodeDefault from '../default' + +// Simple mock translation function +const mockT = (key: string, params?: any) => { + if (key.includes('fieldRequired')) return `${params?.field} is required` + return key +} + +describe('Webhook Trigger Node Default', () => { + describe('Basic Configuration', () => { + it('should have correct default values for all backend fields', () => { + const defaultValue = nodeDefault.defaultValue + + // Core webhook configuration + expect(defaultValue.webhook_url).toBe('') + expect(defaultValue.method).toBe('POST') + expect(defaultValue['content-type']).toBe('application/json') + + // Response configuration fields + expect(defaultValue.async_mode).toBe(true) + expect(defaultValue.status_code).toBe(200) + expect(defaultValue.response_body).toBe('') + + // Parameter arrays + expect(Array.isArray(defaultValue.headers)).toBe(true) + expect(Array.isArray(defaultValue.params)).toBe(true) + expect(Array.isArray(defaultValue.body)).toBe(true) + expect(Array.isArray(defaultValue.default_value)).toBe(true) + + // Initial arrays should be empty + expect(defaultValue.headers).toHaveLength(0) + expect(defaultValue.params).toHaveLength(0) + expect(defaultValue.body).toHaveLength(0) + expect(defaultValue.default_value).toHaveLength(0) + }) + + it('should have empty prev nodes', () => { + const prevNodes = nodeDefault.getAvailablePrevNodes(false) + expect(prevNodes).toEqual([]) + }) + + it('should have available next nodes excluding Start', () => { + const nextNodes = nodeDefault.getAvailableNextNodes(false) + expect(nextNodes).toBeDefined() + expect(nextNodes.length).toBeGreaterThan(0) + }) + }) + + describe('Validation - checkValid', () => { + it('should validate successfully with default configuration', () => { + const payload = nodeDefault.defaultValue + + const result = nodeDefault.checkValid(payload, mockT) + expect(result.isValid).toBe(true) + expect(result.errorMessage).toBe('') + }) + + it('should handle response configuration fields', () => { + const payload = { + ...nodeDefault.defaultValue, + status_code: 404, + response_body: '{"error": "Not found"}', + } + + const result = nodeDefault.checkValid(payload, mockT) + expect(result.isValid).toBe(true) + }) + + it('should handle async_mode field correctly', () => { + const payload = { + ...nodeDefault.defaultValue, + async_mode: false, + } + + const result = nodeDefault.checkValid(payload, mockT) + expect(result.isValid).toBe(true) + }) + }) +}) diff --git a/web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx b/web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx index 9bad9036bb..2b784a28a2 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx @@ -5,6 +5,7 @@ import { RiDeleteBinLine } from '@remixicon/react' import Input from '@/app/components/base/input' import Checkbox from '@/app/components/base/checkbox' import { SimpleSelect } from '@/app/components/base/select' +import { replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import cn from '@/utils/classnames' // Column configuration types for table components @@ -60,9 +61,6 @@ const GenericTable: FC = ({ className, showHeader = false, }) => { - const DELETE_COL_PADDING_CLASS = 'pr-[56px]' - const DELETE_COL_WIDTH_CLASS = 'w-[56px]' - // Build the rows to display while keeping a stable mapping to original data const displayRows = useMemo(() => { // Helper to check empty @@ -131,7 +129,18 @@ const GenericTable: FC = ({ return ( handleChange(e.target.value)} + onChange={(e) => { + // Format variable names (replace spaces with underscores) + if (column.key === 'key' || column.key === 'name') + replaceSpaceWithUnderscoreInVarNameInput(e.target) + handleChange(e.target.value) + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + e.currentTarget.blur() + } + }} placeholder={column.placeholder} disabled={readonly} wrapperClassName="w-full min-w-0" @@ -139,7 +148,7 @@ const GenericTable: FC = ({ // Ghost/inline style: looks like plain text until focus/hover 'h-6 rounded-none border-0 bg-transparent px-0 py-0 shadow-none', 'hover:border-transparent hover:bg-transparent focus:border-transparent focus:bg-transparent', - 'system-sm-regular text-text-secondary placeholder:text-text-tertiary', + 'system-sm-regular text-text-secondary placeholder:text-text-quaternary', )} /> ) @@ -158,20 +167,21 @@ const GenericTable: FC = ({ 'h-6 rounded-none bg-transparent px-0 text-text-secondary', 'hover:bg-transparent focus-visible:bg-transparent group-hover/simple-select:bg-transparent', )} - optionWrapClassName="rounded-md" + optionWrapClassName="w-26 min-w-26 z-[5] -ml-3" notClearable /> ) case 'switch': return ( - handleChange(!value)} - disabled={readonly} - className="!h-4 !w-4 shadow-none" - /> +
+ handleChange(!value)} + disabled={readonly} + /> +
) case 'custom': @@ -184,73 +194,64 @@ const GenericTable: FC = ({ const renderTable = () => { return ( -
+
{showHeader && ( -
- {columns.map(column => ( +
+ {columns.map((column, index) => (
- {column.title} + {column.title}
))}
)}
{displayRows.map(({ row, dataIndex, isVirtual }, renderIndex) => { - // Determine emptiness for UI-only controls visibility - const isEmpty = Object.values(row).every(value => - value === '' || value === null || value === undefined || value === false, - ) - 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() !== '' + return (
- {columns.map(column => ( + {columns.map((column, columnIndex) => (
{renderCell(column, row, dataIndex)}
))} - {!readonly && data.length > 1 && !isEmpty && !isVirtual && ( - {showPlaceholder ? ( -
+
{placeholder}
) : ( diff --git a/web/app/components/workflow/nodes/trigger-webhook/components/header-table.tsx b/web/app/components/workflow/nodes/trigger-webhook/components/header-table.tsx index 8d00a33b9c..1fca4060a4 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/components/header-table.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/components/header-table.tsx @@ -32,7 +32,7 @@ const HeaderTable: FC = ({ key: 'required', title: 'Required', type: 'switch', - width: 'w-[48px]', + width: 'w-[88px]', }, ] @@ -53,9 +53,9 @@ const HeaderTable: FC = ({ // Handle data changes const handleDataChange = (data: GenericTableRow[]) => { const newHeaders: WebhookHeader[] = data - .filter(row => row.name && row.name.trim() !== '') + .filter(row => row.name && typeof row.name === 'string' && row.name.trim() !== '') .map(row => ({ - name: row.name || '', + name: (row.name as string) || '', required: !!row.required, })) onChange(newHeaders) diff --git a/web/app/components/workflow/nodes/trigger-webhook/components/paragraph-input.tsx b/web/app/components/workflow/nodes/trigger-webhook/components/paragraph-input.tsx new file mode 100644 index 0000000000..f3946f5d3d --- /dev/null +++ b/web/app/components/workflow/nodes/trigger-webhook/components/paragraph-input.tsx @@ -0,0 +1,57 @@ +'use client' +import type { FC } from 'react' +import React, { useRef } from 'react' +import cn from '@/utils/classnames' + +type ParagraphInputProps = { + value: string + onChange: (value: string) => void + placeholder?: string + disabled?: boolean + className?: string +} + +const ParagraphInput: FC = ({ + value, + onChange, + placeholder, + disabled = false, + className, +}) => { + const textareaRef = useRef(null) + + const lines = value ? value.split('\n') : [''] + const lineCount = Math.max(3, lines.length) + + return ( +
+
+
+ {Array.from({ length: lineCount }, (_, index) => ( + + {String(index + 1).padStart(2, '0')} + + ))} +
+