mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 19:09:21 +08:00
fix: enhance webhook trigger panel UI consistency and user experience (#24780)
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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<GenericTableProps> = ({
|
||||
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<DisplayRow[]>(() => {
|
||||
// Helper to check empty
|
||||
@@ -131,7 +129,18 @@ const GenericTable: FC<GenericTableProps> = ({
|
||||
return (
|
||||
<Input
|
||||
value={(value as string) || ''}
|
||||
onChange={e => 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<GenericTableProps> = ({
|
||||
// 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<GenericTableProps> = ({
|
||||
'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 (
|
||||
<Checkbox
|
||||
id={`${column.key}-${String(dataIndex ?? 'v')}`}
|
||||
checked={Boolean(value)}
|
||||
onCheck={() => handleChange(!value)}
|
||||
disabled={readonly}
|
||||
className="!h-4 !w-4 shadow-none"
|
||||
/>
|
||||
<div className="flex h-7 items-center">
|
||||
<Checkbox
|
||||
id={`${column.key}-${String(dataIndex ?? 'v')}`}
|
||||
checked={Boolean(value)}
|
||||
onCheck={() => handleChange(!value)}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'custom':
|
||||
@@ -184,73 +194,64 @@ const GenericTable: FC<GenericTableProps> = ({
|
||||
|
||||
const renderTable = () => {
|
||||
return (
|
||||
<div className="rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs">
|
||||
<div className="rounded-lg border border-divider-regular">
|
||||
{showHeader && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 border-b border-divider-subtle px-3 py-2',
|
||||
!readonly && DELETE_COL_PADDING_CLASS,
|
||||
)}
|
||||
>
|
||||
{columns.map(column => (
|
||||
<div className="system-xs-medium-uppercase flex h-7 items-center leading-7 text-text-tertiary">
|
||||
{columns.map((column, index) => (
|
||||
<div
|
||||
key={column.key}
|
||||
className={cn(
|
||||
'text-xs uppercase text-text-tertiary',
|
||||
column.width && column.width.startsWith('w-') ? 'shrink-0' : 'min-w-0 flex-1 overflow-hidden',
|
||||
'h-full pl-3',
|
||||
column.width && column.width.startsWith('w-') ? 'shrink-0' : 'flex-1',
|
||||
column.width,
|
||||
// Add right border except for last column
|
||||
index < columns.length - 1 && 'border-r border-divider-regular',
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{column.title}</span>
|
||||
{column.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="divide-y divide-divider-subtle">
|
||||
{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 (
|
||||
<div
|
||||
key={rowKey}
|
||||
className={cn(
|
||||
'group relative flex items-center gap-2 px-3 py-1.5 hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
!readonly && DELETE_COL_PADDING_CLASS,
|
||||
'group relative flex border-t border-divider-regular',
|
||||
hasContent ? 'hover:bg-state-destructive-hover' : 'hover:bg-state-base-hover',
|
||||
)}
|
||||
style={{ minHeight: '28px' }}
|
||||
>
|
||||
{columns.map(column => (
|
||||
{columns.map((column, columnIndex) => (
|
||||
<div
|
||||
key={column.key}
|
||||
className={cn(
|
||||
'relative',
|
||||
column.width && column.width.startsWith('w-') ? 'shrink-0' : 'min-w-0 flex-1',
|
||||
'shrink-0 pl-3',
|
||||
column.width,
|
||||
// Avoid children overflow when content is long in flexible columns
|
||||
!(column.width && column.width.startsWith('w-')) && 'overflow-hidden',
|
||||
// Add right border except for last column
|
||||
columnIndex < columns.length - 1 && 'border-r border-divider-regular',
|
||||
)}
|
||||
>
|
||||
{renderCell(column, row, dataIndex)}
|
||||
</div>
|
||||
))}
|
||||
{!readonly && data.length > 1 && !isEmpty && !isVirtual && (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-y-0 right-0 hidden items-center justify-end rounded-lg bg-gradient-to-l from-components-panel-on-panel-item-bg to-background-gradient-mask-transparent pr-2 group-hover:pointer-events-auto group-hover:flex',
|
||||
DELETE_COL_WIDTH_CLASS,
|
||||
)}
|
||||
>
|
||||
{!readonly && dataIndex !== null && hasContent && (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dataIndex !== null && removeRow(dataIndex)}
|
||||
className="text-text-tertiary opacity-70 transition-colors hover:text-text-destructive hover:opacity-100"
|
||||
onClick={() => removeRow(dataIndex)}
|
||||
className="p-1"
|
||||
aria-label="Delete row"
|
||||
>
|
||||
<RiDeleteBinLine className="h-3.5 w-3.5" />
|
||||
<RiDeleteBinLine className="h-3.5 w-3.5 text-text-destructive" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -272,7 +273,7 @@ const GenericTable: FC<GenericTableProps> = ({
|
||||
</div>
|
||||
|
||||
{showPlaceholder ? (
|
||||
<div className="py-8 text-center text-sm text-text-tertiary">
|
||||
<div className="flex h-7 items-center justify-center rounded-lg border border-divider-regular bg-components-panel-bg text-xs font-normal leading-[18px] text-text-quaternary">
|
||||
{placeholder}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -32,7 +32,7 @@ const HeaderTable: FC<HeaderTableProps> = ({
|
||||
key: 'required',
|
||||
title: 'Required',
|
||||
type: 'switch',
|
||||
width: 'w-[48px]',
|
||||
width: 'w-[88px]',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -53,9 +53,9 @@ const HeaderTable: FC<HeaderTableProps> = ({
|
||||
// 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)
|
||||
|
||||
@@ -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<ParagraphInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
className,
|
||||
}) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const lines = value ? value.split('\n') : ['']
|
||||
const lineCount = Math.max(3, lines.length)
|
||||
|
||||
return (
|
||||
<div className={cn('rounded-xl bg-components-input-bg-normal px-3 pb-2 pt-3', className)}>
|
||||
<div className="relative">
|
||||
<div className="pointer-events-none absolute left-0 top-0 flex flex-col">
|
||||
{Array.from({ length: lineCount }, (_, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="flex h-[20px] select-none items-center font-mono text-xs leading-[20px] text-text-quaternary"
|
||||
>
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className="w-full resize-none border-0 bg-transparent pl-6 font-mono text-xs leading-[20px] text-text-secondary outline-none placeholder:text-text-quaternary"
|
||||
style={{
|
||||
minHeight: `${Math.max(3, lineCount) * 20}px`,
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
rows={Math.max(3, lineCount)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ParagraphInput)
|
||||
@@ -63,7 +63,7 @@ const ParameterTable: FC<ParameterTableProps> = ({
|
||||
key: 'type',
|
||||
title: 'Type',
|
||||
type: (isRequestBody ? 'select' : 'input') as ColumnConfig['type'],
|
||||
width: 'w-[96px]',
|
||||
width: 'w-[78px]',
|
||||
placeholder: 'Type',
|
||||
options: isRequestBody ? typeOptions : undefined,
|
||||
}]
|
||||
@@ -72,14 +72,14 @@ const ParameterTable: FC<ParameterTableProps> = ({
|
||||
key: 'required',
|
||||
title: 'Required',
|
||||
type: 'switch',
|
||||
width: 'w-[48px]',
|
||||
width: 'w-[88px]',
|
||||
},
|
||||
]
|
||||
|
||||
// Empty row template for new rows
|
||||
const emptyRowData: GenericTableRow = {
|
||||
key: '',
|
||||
type: '',
|
||||
type: isRequestBody ? 'string' : '',
|
||||
required: false,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import type { HttpMethod, WebhookTriggerNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import ParameterTable from './components/parameter-table'
|
||||
import HeaderTable from './components/header-table'
|
||||
import ParagraphInput from './components/paragraph-input'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import InputWithCopy from '@/app/components/base/input-with-copy'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import copy from 'copy-to-clipboard'
|
||||
@@ -50,7 +50,6 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
handleHeadersChange,
|
||||
handleParamsChange,
|
||||
handleBodyChange,
|
||||
handleAsyncModeChange,
|
||||
handleStatusCodeChange,
|
||||
handleResponseBodyChange,
|
||||
generateWebhookUrl,
|
||||
@@ -71,7 +70,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
{/* Webhook URL Section */}
|
||||
<Field title={t(`${i18nPrefix}.webhookUrl`)}>
|
||||
<div className="space-y-1">
|
||||
<div className="flex gap-1" style={{ width: '368px', height: '32px' }}>
|
||||
<div className="flex gap-1" style={{ height: '32px' }}>
|
||||
<div className="w-26 shrink-0">
|
||||
<SimpleSelect
|
||||
items={HTTP_METHODS}
|
||||
@@ -105,7 +104,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
popupClassName="system-xs-regular text-text-primary bg-components-tooltip-bg border border-components-panel-border shadow-lg backdrop-blur-sm rounded-md px-1.5 py-1"
|
||||
position="top"
|
||||
offset={{ mainAxis: -20 }}
|
||||
needsDelay={false}
|
||||
needsDelay={true}
|
||||
>
|
||||
<div
|
||||
className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 transition-colors"
|
||||
@@ -130,9 +129,6 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
)}
|
||||
</div>
|
||||
</Field>
|
||||
<span>{inputs.webhook_debug_url || ''}</span>
|
||||
|
||||
<Split />
|
||||
|
||||
{/* Content Type */}
|
||||
<Field title={t(`${i18nPrefix}.contentType`)}>
|
||||
@@ -151,8 +147,6 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Split />
|
||||
|
||||
{/* Query Parameters */}
|
||||
<ParameterTable
|
||||
readonly={readOnly}
|
||||
@@ -163,8 +157,6 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
showType={false}
|
||||
/>
|
||||
|
||||
<Split />
|
||||
|
||||
{/* Header Parameters */}
|
||||
<HeaderTable
|
||||
readonly={readOnly}
|
||||
@@ -172,8 +164,6 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
onChange={handleHeadersChange}
|
||||
/>
|
||||
|
||||
<Split />
|
||||
|
||||
{/* Request Body Parameters */}
|
||||
<ParameterTable
|
||||
readonly={readOnly}
|
||||
@@ -191,17 +181,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
<Field title={t(`${i18nPrefix}.responseConfiguration`)}>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="system-sm-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.asyncMode`)}
|
||||
</span>
|
||||
<Switch
|
||||
defaultValue={inputs.async_mode}
|
||||
onChange={handleAsyncModeChange}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="system-sm-medium mb-2 block text-text-secondary">
|
||||
<label className="system-sm-medium text-text-tertiary">
|
||||
{t(`${i18nPrefix}.statusCode`)}
|
||||
</label>
|
||||
<Input
|
||||
@@ -209,15 +189,18 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
value={inputs.status_code}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleStatusCodeChange(Number(e.target.value))}
|
||||
disabled={readOnly}
|
||||
wrapperClassName="w-[120px]"
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="system-sm-medium mb-2 block text-text-secondary">
|
||||
<label className="system-sm-medium mb-2 block text-text-tertiary">
|
||||
{t(`${i18nPrefix}.responseBody`)}
|
||||
</label>
|
||||
<Input
|
||||
<ParagraphInput
|
||||
value={inputs.response_body}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleResponseBodyChange(e.target.value)}
|
||||
onChange={handleResponseBodyChange}
|
||||
placeholder={t(`${i18nPrefix}.responseBodyPlaceholder`)}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1022,11 +1022,11 @@ const translation = {
|
||||
debugUrlCopied: 'Copied!',
|
||||
errorHandling: 'Error Handling',
|
||||
errorStrategy: 'Error Handling',
|
||||
responseConfiguration: 'Response Configuration',
|
||||
responseConfiguration: 'Response',
|
||||
asyncMode: 'Async Mode',
|
||||
statusCode: 'Status Code',
|
||||
responseBody: 'Response Body',
|
||||
responseBodyPlaceholder: 'Response body content',
|
||||
responseBodyPlaceholder: 'Write your response body here',
|
||||
headers: 'Headers',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1022,11 +1022,11 @@ const translation = {
|
||||
debugUrlCopied: 'コピーしました!',
|
||||
errorHandling: 'エラー処理',
|
||||
errorStrategy: 'エラー処理',
|
||||
responseConfiguration: 'レスポンス設定',
|
||||
responseConfiguration: 'レスポンス',
|
||||
asyncMode: '非同期モード',
|
||||
statusCode: 'ステータスコード',
|
||||
responseBody: 'レスポンスボディ',
|
||||
responseBodyPlaceholder: 'レスポンス本文',
|
||||
responseBodyPlaceholder: 'ここにレスポンスボディを入力してください',
|
||||
headers: 'ヘッダー',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1022,11 +1022,11 @@ const translation = {
|
||||
debugUrlCopied: '已复制!',
|
||||
errorHandling: '错误处理',
|
||||
errorStrategy: '错误处理',
|
||||
responseConfiguration: '响应配置',
|
||||
responseConfiguration: '响应',
|
||||
asyncMode: '异步模式',
|
||||
statusCode: '状态码',
|
||||
responseBody: '响应体',
|
||||
responseBodyPlaceholder: '响应体内容',
|
||||
responseBodyPlaceholder: '在此输入您的响应体',
|
||||
headers: 'Headers',
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user