perf(json-editor): unify admin JSON editing experience (#6421)

* perf(json-editor): improve JSON editing experience

- integrate Yace for syntax highlighting, history, indentation, auto-closing, and smart line breaks.
- add copy support, cursor location feedback, and synchronized content and line-number scrolling.
- extract JSON editor utilities and cover key interactions with unit tests.

* perf(system-settings): improve JSON configuration editing

- replace raw JSON textareas with the shared editor for highlighting, validation, copy, and formatting.
- preserve field-specific examples and make placeholders visible through the transparent editor layer.
- remove duplicate formatting controls while keeping existing form validation and save behavior.

* perf(json-editor): standardize JSON inputs across admin settings

- replace pure JSON textareas with the shared editor across system settings and channel workflows.
- preserve form focus, validation, placeholders, and visual or JSON editing modes.
- add happy-dom component coverage for form bindings, controlled updates, and formatting.

* fix(json-code-editor): address accessibility review findings

- Drop the unconditional aria-label that overrode every field's
  label-derived accessible name; add an optional ariaLabel prop and
  set it at call sites without an associated label
- Associate standalone Labels via htmlFor/id in channel-affinity views
- Hide the highlight mirror and line-number layers from the
  accessibility tree (aria-hidden)
- Give the line-number gutter an opaque background so horizontally
  scrolled code no longer slides under it
- Degrade to no scroll sync instead of destroying the editor when the
  line-number layer is not found
This commit is contained in:
QuentinHsu
2026-07-25 19:10:28 +08:00
committed by GitHub
parent 18b0b7631a
commit eb4a1bd193
28 changed files with 1089 additions and 419 deletions
@@ -33,6 +33,7 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Dialog } from '@/components/dialog'
import { JsonCodeEditor } from '@/components/json-code-editor'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -54,7 +55,6 @@ import {
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea'
import {
Tooltip,
TooltipContent,
@@ -461,12 +461,6 @@ export function AdvancedCustomEditorDialog({
if (jsonError) setJsonError('')
}
const formatJson = () => {
const parsed = parseJsonEditorConfig()
if (!parsed) return
setJsonText(stringifyAdvancedCustomConfig(parsed))
}
const applyTemplate = (mode: 'fill' | 'append') => {
const templateConfig = getAdvancedCustomTemplateConfig(templateKey)
let nextConfig = templateConfig
@@ -713,26 +707,19 @@ export function AdvancedCustomEditorDialog({
) : (
<div className='p-4'>
<div className='mb-2 flex items-center gap-2'>
<Button
type='button'
variant='outline'
size='sm'
onClick={formatJson}
>
{t('Format')}
</Button>
<span className='text-muted-foreground text-xs'>
{t('Advanced text editing')}
</span>
</div>
<Textarea
<JsonCodeEditor
value={jsonText}
onChange={(event) => handleJsonChange(event.target.value)}
onChange={handleJsonChange}
placeholder={stringifyAdvancedCustomConfig(
getAdvancedCustomTemplateConfig(templateKey)
)}
rows={22}
className='min-h-[420px] font-mono text-xs'
heightClassName='h-[420px] min-h-[420px] max-h-[420px]'
aria-invalid={Boolean(jsonError)}
ariaLabel={t('Advanced text editing')}
/>
<p className='text-muted-foreground mt-2 text-xs'>
{t('Edit JSON text directly. Format will be validated on save.')}
@@ -24,6 +24,7 @@ import { toast } from 'sonner'
import { Dialog } from '@/components/dialog'
import { GroupBadge } from '@/components/group-badge'
import { JsonCodeEditor } from '@/components/json-code-editor'
import { StatusBadge } from '@/components/status-badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -38,7 +39,6 @@ import {
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea'
import {
editTagChannels,
@@ -370,13 +370,12 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
{t('(Optional: redirect model names)')}
</span>
</Label>
<Textarea
<JsonCodeEditor
id='model-mapping'
value={modelMapping}
onChange={(e) => setModelMapping(e.target.value)}
onChange={setModelMapping}
placeholder={'{\n "gpt-3.5-turbo": "gpt-3.5-turbo-0125"\n}'}
rows={4}
className='font-mono text-sm'
heightClassName='h-40 min-h-40 max-h-40'
/>
<div className='flex gap-2'>
<Button
@@ -37,6 +37,7 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Dialog } from '@/components/dialog'
import { JsonCodeEditor } from '@/components/json-code-editor'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
@@ -1656,17 +1657,6 @@ export function ParamOverrideEditorDialog(
[t]
)
const formatJson = useCallback(() => {
const trimmed = jsonText.trim()
if (!trimmed) return
if (!verifyJSON(trimmed)) {
toast.error(t('Parameter override must be valid JSON format'))
return
}
setJsonText(JSON.stringify(JSON.parse(trimmed), null, 2))
setJsonError('')
}, [jsonText, t])
const visualValidationError = useMemo(() => {
if (editMode !== 'visual') return ''
try {
@@ -1832,12 +1822,12 @@ export function ParamOverrideEditorDialog(
<p className='text-muted-foreground mb-2 text-sm'>
{t('Legacy Format (JSON Object)')}
</p>
<Textarea
<JsonCodeEditor
value={legacyValue}
onChange={(e) => setLegacyValue(e.target.value)}
onChange={setLegacyValue}
placeholder={JSON.stringify(LEGACY_TEMPLATE, null, 2)}
rows={14}
className='font-mono text-xs'
heightClassName='h-72 min-h-72 max-h-72'
ariaLabel={t('Legacy Format (JSON Object)')}
/>
<p className='text-muted-foreground mt-2 text-xs'>
{t(
@@ -2044,24 +2034,17 @@ export function ParamOverrideEditorDialog(
/* JSON mode */
<div className='p-4'>
<div className='mb-2 flex items-center gap-2'>
<Button
type='button'
variant='outline'
size='sm'
onClick={formatJson}
>
{t('Format')}
</Button>
<span className='text-muted-foreground text-xs'>
{t('Advanced text editing')}
</span>
</div>
<Textarea
<JsonCodeEditor
value={jsonText}
onChange={(e) => handleJsonChange(e.target.value)}
onChange={handleJsonChange}
placeholder={JSON.stringify(OPERATION_TEMPLATE, null, 2)}
rows={20}
className='font-mono text-xs'
heightClassName='h-[420px] min-h-[420px] max-h-[420px]'
aria-invalid={Boolean(jsonError)}
ariaLabel={t('Advanced text editing')}
/>
<p className='text-muted-foreground mt-2 text-xs'>
{t('Edit JSON text directly. Format will be validated on save.')}