diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 3da74613fe3..d0d06b284d2 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1937,6 +1937,9 @@ "web/app/components/base/markdown-blocks/form.tsx": { "no-restricted-imports": { "count": 1 + }, + "prefer-regex-literals": { + "count": 1 } }, "web/app/components/base/markdown-blocks/index.ts": { diff --git a/web/app/components/base/markdown-blocks/__tests__/form.spec.tsx b/web/app/components/base/markdown-blocks/__tests__/form.spec.tsx index 34259831026..d21fc81878c 100644 --- a/web/app/components/base/markdown-blocks/__tests__/form.spec.tsx +++ b/web/app/components/base/markdown-blocks/__tests__/form.spec.tsx @@ -532,6 +532,54 @@ describe('MarkdownForm', () => { }) }) + // Unicode letters should be valid form field names. + describe('Unicode name support', () => { + it('should include Unicode-named fields from all supported controls in JSON output', async () => { + const user = userEvent.setup() + const node = createRootNode( + [ + createElementNode('label', { for: '用户名' }, [createTextNode('Username:')]), + createElementNode('input', { type: 'text', name: '用户名', value: 'Alice' }), + createElementNode('label', { for: '密码' }, [createTextNode('Password:')]), + createElementNode('input', { type: 'password', name: '密码', value: 'secret' }), + createElementNode('label', { for: '内容' }, [createTextNode('Content:')]), + createElementNode('textarea', { name: '内容', value: 'Hello' }), + createElementNode('label', { for: '日期' }, [createTextNode('Date:')]), + createElementNode('input', { type: 'date', name: '日期', value: dayjs('2026-01-10') }), + createElementNode('label', { for: '时间' }, [createTextNode('Time:')]), + createElementNode('input', { type: 'time', name: '时间', value: '09:00' }), + createElementNode('label', { for: '日期时间' }, [createTextNode('Datetime:')]), + createElementNode('input', { type: 'datetime', name: '日期时间', value: dayjs('2026-01-10T08:30:00') }), + createElementNode('label', { for: 'café' }, [createTextNode('Select:')]), + createElementNode('input', { type: 'select', name: 'café', value: 'hello', dataOptions: ['hello', 'world'] }), + createElementNode('input', { type: 'checkbox', name: '同意条款', value: true, dataTip: 'By checking this means you agreed' }), + createElementNode('button', { dataSize: 'small', dataVariant: 'primary' }, [createTextNode('Login')]), + ], + { dataFormat: 'json' }, + ) + + render() + + await user.click(screen.getByRole('button', { name: 'Login' })) + + await waitFor(() => { + expect(mockOnSend).toHaveBeenCalled() + }) + + const submittedPayload = JSON.parse(mockOnSend.mock.calls[0]![0]) as Record + expect(submittedPayload).toMatchObject({ + 用户名: 'Alice', + 密码: 'secret', + 内容: 'Hello', + 日期: 'formatted-date', + 日期时间: 'formatted-datetime', + café: 'hello', + 同意条款: true, + }) + expect(submittedPayload).toHaveProperty('时间') + }) + }) + // Double-click protection: button disables after the first submit. describe('Double submit prevention', () => { it('should disable submit button after first click', async () => { diff --git a/web/app/components/base/markdown-blocks/form.tsx b/web/app/components/base/markdown-blocks/form.tsx index d5a2102101e..46a3ef5d9ea 100644 --- a/web/app/components/base/markdown-blocks/form.tsx +++ b/web/app/components/base/markdown-blocks/form.tsx @@ -41,7 +41,15 @@ type SupportedType = typeof SUPPORTED_TYPES[keyof typeof SUPPORTED_TYPES] const SUPPORTED_TYPES_SET = new Set(Object.values(SUPPORTED_TYPES)) -const SAFE_NAME_RE = /^[a-z][\w-]*$/i +const SAFE_NAME_RE = (() => { + try { + return new RegExp('^\\p{L}[\\p{L}\\p{M}\\p{N}_-]*$', 'u') + } + catch { + // Fallback for browsers without Unicode property escape support. + return /^[a-z][\w-]*$/i + } +})() const PROTOTYPE_POISON_KEYS = new Set(['__proto__', 'constructor', 'prototype']) function isSafeName(name: unknown): name is string {