import { useState } from 'react' import type { JSONSchema } from '../api' import { Icon } from '../ui/Icon' import { useForm } from './context' import { Widget } from './Form' import { defaultFor, resolveSchema } from './resolve' const isObj = (v: unknown): v is Record => !!v && typeof v === 'object' && !Array.isArray(v) /** * Key → value editor: `additionalProperties` schemas, `x-editor: dictionary`, objects without a * declared shape. With `x-dict-keys` the keys are a fixed vocabulary (one row each, blank removes); * otherwise keys are free-form (rename, add, remove). */ export function MapField({ schema: s, value, onChange, path }: { schema: JSONSchema; value: unknown; onChange(v: unknown): void; path: string }) { const { root } = useForm() const obj = isObj(value) ? value : {} const ap = s.additionalProperties const valueSchema: JSONSchema = ap && typeof ap === 'object' ? ap : {} const fixed = Array.isArray(s['x-dict-keys']) ? (s['x-dict-keys'] as string[]) : null const [draftKey, setDraftKey] = useState('') const [dupe, setDupe] = useState(false) const setVal = (k: string, v: unknown) => { const next = { ...obj } if (fixed && (v === undefined || v === '')) delete next[k] else next[k] = v === undefined ? defaultFor(root, valueSchema) : v onChange(next) } if (fixed) { const rest = Object.keys(obj).filter((k) => !fixed.includes(k)) return (
{[...fixed, ...rest].map((k) => (
{k}
setVal(k, v)} path={`${path}.${k}`} />
{k in obj && ( )}
))}
) } const rename = (from: string, to: string) => { to = to.trim() if (!to || to === from) return false if (to in obj) return setDupe(true), false // Rebuild to keep the row's position. onChange(Object.fromEntries(Object.entries(obj).map(([k, v]) => [k === from ? to : k, v]))) return true } const add = () => { const k = draftKey.trim() if (!k) return if (k in obj) return setDupe(true) onChange({ ...obj, [k]: defaultFor(root, valueSchema) }) setDraftKey('') setDupe(false) } return (
{Object.entries(obj).map(([k, v]) => (
rename(k, to)} />
setVal(k, nv)} path={`${path}.${k}`} required />
))}
(setDraftKey(e.target.value), setDupe(false))} onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), add())} />
{dupe && That key already exists}
) } /** Key text is local until blur/Enter: renaming rebuilds the object, which would otherwise steal focus per keystroke. */ function KeyInput({ name, onCommit }: { name: string; onCommit(to: string): boolean }) { const [text, setText] = useState(name) const [seen, setSeen] = useState(name) if (seen !== name) { setSeen(name) setText(name) } const commit = () => (onCommit(text) ? undefined : setText(name)) return setText(e.target.value)} onBlur={commit} onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), commit())} /> }