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:
@@ -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.')}
|
||||
|
||||
Reference in New Issue
Block a user