import { useEffect, useMemo, useState } from "react";import * as Option from "effect/Option";import * as Schema from "effect/Schema"; import { safeSchemaValueLabel } from "./schema-explorer";import { Input } from "./input";import { Switch } from "./switch";import { NativeSelect, NativeSelectOption } from "./native-select";import { Label } from "./label";import { Textarea } from "./textarea";import { Button } from "./button";import { Badge } from "./badge";import { cn } from "../lib/utils"; // ---------------------------------------------------------------------------// JsonSchemaForm — a dynamic form built from a tool's input JSON Schema. The// single source of truth is the parent's args object (controlled `value`); this// renders a pure view and emits a fully-rebuilt object on every edit. Anything// the renderer can't express as a typed control falls back to a raw-JSON subtree// editor, so no input shape is ever un-editable.//// Constraints honored: only wrapped design-system components (never raw// <input>/<select>/<button>/<label>/<textarea>/<option>); no raw `JSON.parse`// (parsing goes through `decodeJson`, an Option-returning Effect Schema decode);// every callback param explicitly typed (tsgo can't infer through the generics).// --------------------------------------------------------------------------- // JSON Schema subset we render. Mirrors the type in `schema-explorer.tsx`; the// ref-resolution helpers below mirror that file too (kept local to avoid a// cross-file public-API coupling for ~25 lines).type JsonSchema = { type?: string | string[]; properties?: Record<string, JsonSchema>; required?: string[]; items?: JsonSchema | JsonSchema[]; additionalProperties?: boolean | JsonSchema; oneOf?: JsonSchema[]; anyOf?: JsonSchema[]; allOf?: JsonSchema[]; enum?: unknown[]; const?: unknown; $ref?: string; $defs?: Record<string, JsonSchema>; description?: unknown; title?: unknown; default?: unknown; nullable?: boolean; format?: string;}; /** Decode a JSON string at the boundary. Effect Schema is used because the repo * forbids raw `JSON.parse`; a malformed string yields `None`. */const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString); /** Sentinel a field emits to ask its object parent to delete its key. */const REMOVE = Symbol("json-schema-form/remove"); // --- Ref resolution (mirrors schema-explorer.tsx) -------------------------- const resolveRef = (ref: string, root: JsonSchema): JsonSchema | null => { const name = ref.match(/^#\/\$defs\/(.+)$/)?.[1]; if (!name || !root.$defs) return null; return root.$defs[name] ?? null;}; /** Fully resolve a schema, following `$ref` and unwrapping single-variant * oneOf/anyOf so we can inspect the concrete shape. */const deepResolve = (schema: JsonSchema, root: JsonSchema): JsonSchema => { let s = schema; if (s.$ref) { const resolved = resolveRef(s.$ref, root); if (resolved) s = resolved; } if (s.oneOf?.length === 1) s = deepResolve(s.oneOf[0]!, root); if (s.anyOf?.length === 1) s = deepResolve(s.anyOf[0]!, root); return s;}; // --- Small schema utilities ------------------------------------------------ const asObjectSchema = (value: unknown): JsonSchema | undefined => value && typeof value === "object" && !Array.isArray(value) ? (value as JsonSchema) : undefined; const schemaTypes = (schema: JsonSchema): readonly string[] => Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []; /** The non-null primitive type for `type: ["string","null"]` / `nullable`. */const primaryType = (schema: JsonSchema): string | undefined => schemaTypes(schema).find((t: string) => t !== "null"); const schemaDescription = (schema: JsonSchema): string | undefined => { const d = schema.description; if (typeof d === "string") return d; if (Option.isOption(d) && Option.isSome(d) && typeof d.value === "string") return d.value; return undefined;}; export const emptyEnumGuidance = (description: string | undefined): string => { const integration = description?.match(/^Connection to use for .+ \(([^)]+)\)$/)?.[1]; return integration ? `Connect ${integration} first.` : "No options available.";}; const DEFAULT_BY_TYPE: Record<string, () => unknown> = { string: () => "", number: () => 0, integer: () => 0, boolean: () => false, object: () => ({}), array: () => [],}; /** Seed used when the user clicks "Add" for an optional property or array item. * Schema defaults can also be displayed without being injected into JSON. */export const defaultForSchema = (schema: JsonSchema, root: JsonSchema): unknown => { const s = deepResolve(schema, root); if (s.default !== undefined) return s.default; if (s.const !== undefined) return s.const; if (Array.isArray(s.enum) && s.enum.length > 0) return s.enum[0]; const t = primaryType(s); const make = t ? DEFAULT_BY_TYPE[t] : undefined; return make ? make() : null;}; /** True only when the schema resolves to a renderable object root (a `type: * "object"` with `properties`). Otherwise the Form tab is disabled and the * panel falls back to raw JSON. */export const isRenderableObjectSchema = ( schema: unknown, definitions?: Record<string, unknown>,): boolean => { const obj = asObjectSchema(schema); if (!obj) return false; const root: JsonSchema = { $defs: (definitions ?? {}) as Record<string, JsonSchema>, }; const resolved = deepResolve(obj, root); return resolved.type === "object" || resolved.properties !== undefined;}; /** Required top-level fields not yet satisfied by `value` — a field counts as * satisfied when it is present and not undefined/null/empty-string. Empty array * ⇒ all required args supplied; drives the Run button's enabled state. */export const missingRequiredFields = ( schema: unknown, definitions: Record<string, unknown> | undefined, value: Record<string, unknown>,): readonly string[] => { const obj = asObjectSchema(schema); if (!obj) return []; const root: JsonSchema = { $defs: (definitions ?? {}) as Record<string, JsonSchema> }; const resolved = deepResolve(obj, root); const required = Array.isArray(resolved.required) ? resolved.required : []; return required.filter((key: string): boolean => { if (!(key in value)) return true; const v = value[key]; return v === undefined || v === null || v === ""; });}; // --- Pure value transforms (unit-tested directly) -------------------------- /** Rebuild an object with `key` set to `next` — never mutates the input, never * produces `undefined` values. */export const setProperty = ( value: Record<string, unknown>, key: string, next: unknown,): Record<string, unknown> => ({ ...value, [key]: next }); /** Rebuild an object with `key` deleted. */export const removeProperty = ( value: Record<string, unknown>, key: string,): Record<string, unknown> => { const { [key]: _omit, ...rest } = value; return rest;}; /** Apply a child field's emitted value to its object parent: the REMOVE sentinel * deletes the key, anything else sets it. */const applyChild = ( value: Record<string, unknown>, key: string, next: unknown,): Record<string, unknown> => next === REMOVE ? removeProperty(value, key) : setProperty(value, key, next); /** Coerce a number-input string to a JSON number, or `undefined` if not a finite * number (caller keeps last-valid + marks invalid). */export const coerceNumber = (raw: string, integer: boolean): number | undefined => { if (raw.trim() === "") return undefined; const n = Number(raw); if (!Number.isFinite(n)) return undefined; return integer ? Math.trunc(n) : n;}; // ---------------------------------------------------------------------------// JsonSchemaForm — top-level object renderer// --------------------------------------------------------------------------- export function JsonSchemaForm(props: { readonly schema: unknown; readonly definitions?: Record<string, unknown>; readonly value: Record<string, unknown>; readonly onChange: (next: Record<string, unknown>) => void; readonly disabled?: boolean;}): React.ReactElement { const { schema, definitions, value, onChange, disabled } = props; const root = useMemo<JsonSchema>( () => ({ $defs: (definitions ?? {}) as Record<string, JsonSchema> }), [definitions], ); const resolved = useMemo<JsonSchema>( () => deepResolve(asObjectSchema(schema) ?? {}, root), [schema, root], ); return ( <ObjectFields schema={resolved} root={root} value={value} onChange={onChange} depth={0} disabled={disabled} /> );} // ---------------------------------------------------------------------------// ObjectFields — renders an object's properties (required/default-backed first,// then present optionals, then "Add" rows for absent optionals)// --------------------------------------------------------------------------- function ObjectFields(props: { readonly schema: JsonSchema; readonly root: JsonSchema; readonly value: Record<string, unknown>; readonly onChange: (next: Record<string, unknown>) => void; readonly depth: number; readonly idPrefix?: string; readonly disabled?: boolean;}): React.ReactElement { const { schema, root, value, onChange, depth, idPrefix = "jsf", disabled } = props; const properties = schema.properties ?? {}; const requiredSet = new Set(schema.required ?? []); const entries = Object.entries(properties); const present = entries.filter(([key, propSchema]: [string, JsonSchema]) => { const resolved = deepResolve(propSchema, root); return key in value || requiredSet.has(key) || resolved.default !== undefined; }); const addable = entries.filter( ([key, propSchema]: [string, JsonSchema]) => !requiredSet.has(key) && !(key in value) && deepResolve(propSchema, root).default === undefined, ); // No properties → render nothing (no "takes no arguments" noise); a zero-arg // tool simply runs with an empty payload. if (entries.length === 0) { return <></>; } return ( <div className="space-y-3"> {present.map(([key, propSchema]: [string, JsonSchema]) => { const fieldId = `${idPrefix}-${key}`; return ( <JsonSchemaField key={key} schema={propSchema} root={root} name={key} fieldId={fieldId} required={requiredSet.has(key)} value={value[key]} present={key in value} onChange={(next: unknown) => onChange(applyChild(value, key, next))} depth={depth} disabled={disabled} /> ); })} {addable.length > 0 && ( <div className="flex flex-wrap gap-1.5 pt-1"> {addable.map(([key, propSchema]: [string, JsonSchema]) => ( <Button key={key} type="button" size="xs" variant="outline" disabled={disabled} onClick={() => onChange(setProperty(value, key, defaultForSchema(propSchema, root)))} > + {key} </Button> ))} </div> )} </div> );} // ---------------------------------------------------------------------------// JsonSchemaField — dispatches one property to the right control// --------------------------------------------------------------------------- function JsonSchemaField(props: { readonly schema: JsonSchema; readonly root: JsonSchema; readonly name: string; readonly fieldId: string; readonly required: boolean; readonly value: unknown; /** Whether the key is present in the parent object (drives the Remove button). */ readonly present: boolean; readonly onChange: (next: unknown) => void; readonly depth: number; readonly disabled?: boolean;}): React.ReactElement { const { root, name, fieldId, required, value, present, onChange, depth, disabled } = props; const schema = deepResolve(props.schema, root); const description = schemaDescription(schema); const fieldValue = value === undefined && schema.default !== undefined ? schema.default : value; const header = ( <div className="flex items-center justify-between gap-2"> <Label htmlFor={fieldId} className="text-xs"> <span>{name}</span> {required && ( <Badge variant="secondary" className="px-1.5 py-0 text-[10px]"> required </Badge> )} </Label> {!required && present && ( <Button type="button" size="xs" variant="ghost" className="h-5 px-1.5 text-muted-foreground" disabled={disabled} onClick={() => onChange(REMOVE)} > Remove </Button> )} </div> ); const help = description ? ( <p className="text-[11px] leading-4 text-muted-foreground">{description}</p> ) : null; return ( <div className="space-y-1"> {header} <FieldControl schema={schema} root={root} fieldId={fieldId} value={fieldValue} onChange={onChange} depth={depth} disabled={disabled} /> {help} </div> );} // ---------------------------------------------------------------------------// FieldControl — the actual control for a resolved schema node// --------------------------------------------------------------------------- function FieldControl(props: { readonly schema: JsonSchema; readonly root: JsonSchema; readonly fieldId: string; readonly value: unknown; readonly onChange: (next: unknown) => void; readonly depth: number; readonly disabled?: boolean;}): React.ReactElement { const { schema, root, fieldId, value, onChange, depth, disabled } = props; // Recursion guard / unsupported constructs → raw-JSON subtree fallback. const unsupported = depth > 8 || (schema.oneOf?.length ?? 0) > 1 || (schema.anyOf?.length ?? 0) > 1 || schema.allOf !== undefined || Array.isArray(schema.items) || (schema.$ref !== undefined && resolveRef(schema.$ref, root) === null); if (unsupported) { return ( <JsonFallbackField schema={schema} root={root} fieldId={fieldId} value={value} onChange={onChange} disabled={disabled} /> ); } // const — fixed value, shown disabled and auto-applied. if (schema.const !== undefined) { return ( <ConstField fieldId={fieldId} constValue={schema.const} value={value} onChange={onChange} /> ); } if (Array.isArray(schema.enum) && schema.enum.length === 0) { const guidance = emptyEnumGuidance(schemaDescription(schema)); return ( <div className="space-y-1"> <NativeSelect id={fieldId} className="w-full text-xs" value="" disabled={true}> <NativeSelectOption value="">{guidance}</NativeSelectOption> </NativeSelect> <p className="text-[11px] leading-4 text-muted-foreground">{guidance}</p> </div> ); } // enum — native select. Optional/unset gets a leading empty option. if (Array.isArray(schema.enum) && schema.enum.length > 0) { const options = schema.enum; const currentIndex = options.findIndex((o: unknown) => o === value); return ( <NativeSelect id={fieldId} className="w-full text-xs" value={currentIndex >= 0 ? String(currentIndex) : ""} disabled={disabled} onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { const idx = Number(e.target.value); if (Number.isInteger(idx) && idx >= 0 && idx < options.length) onChange(options[idx]); }} > <NativeSelectOption value="">— select —</NativeSelectOption> {options.map((option: unknown, i: number) => ( <NativeSelectOption key={i} value={String(i)}> {safeSchemaValueLabel(option)} </NativeSelectOption> ))} </NativeSelect> ); } const type = primaryType(schema); if (type === "boolean") { return ( <div className="flex h-9 items-center"> <Switch id={fieldId} checked={value === true} disabled={disabled} onCheckedChange={(checked: boolean) => onChange(checked)} /> </div> ); } if (type === "number" || type === "integer") { return ( <NumberField schema={schema} fieldId={fieldId} value={value} onChange={onChange} integer={type === "integer"} disabled={disabled} /> ); } if (type === "string") { const inputType = schema.format === "email" ? "email" : schema.format === "uri" || schema.format === "url" ? "url" : schema.format === "date" ? "date" : "text"; return ( <Input id={fieldId} type={inputType} className="text-xs" value={typeof value === "string" ? value : ""}Lines 1–500 of 738. Load more
import { useEffect, useMemo, useState } from "react"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { safeSchemaValueLabel } from "./schema-explorer"; import { Input } from "./input"; import { Switch } from "./switch"; import { NativeSelect, NativeSelectOption } from "./native-select"; import { Label } from "./label"; import { Textarea } from "./textarea"; import { Button } from "./button"; import { Badge } from "./badge"; import { cn } from "../lib/utils"; // --------------------------------------------------------------------------- // JsonSchemaForm — a dynamic form built from a tool's input JSON Schema. The // single source of truth is the parent's args object (controlled `value`); this // renders a pure view and emits a fully-rebuilt object on every edit. Anything // the renderer can't express as a typed control falls back to a raw-JSON subtree // editor, so no input shape is ever un-editable. // // Constraints honored: only wrapped design-system components (never raw // <input>/<select>/<button>/<label>/<textarea>/<option>); no raw `JSON.parse` // (parsing goes through `decodeJson`, an Option-returning Effect Schema decode); // every callback param explicitly typed (tsgo can't infer through the generics). // --------------------------------------------------------------------------- // JSON Schema subset we render. Mirrors the type in `schema-explorer.tsx`; the // ref-resolution helpers below mirror that file too (kept local to avoid a // cross-file public-API coupling for ~25 lines). type JsonSchema = { type?: string | string[]; properties?: Record<string, JsonSchema>; required?: string[]; items?: JsonSchema | JsonSchema[]; additionalProperties?: boolean | JsonSchema; oneOf?: JsonSchema[]; anyOf?: JsonSchema[]; allOf?: JsonSchema[]; enum?: unknown[]; const?: unknown; $ref?: string; $defs?: Record<string, JsonSchema>; description?: unknown; title?: unknown; default?: unknown; nullable?: boolean; format?: string; }; /** Decode a JSON string at the boundary. Effect Schema is used because the repo * forbids raw `JSON.parse`; a malformed string yields `None`. */ const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString); /** Sentinel a field emits to ask its object parent to delete its key. */ const REMOVE = Symbol("json-schema-form/remove"); // --- Ref resolution (mirrors schema-explorer.tsx) -------------------------- const resolveRef = (ref: string, root: JsonSchema): JsonSchema | null => { const name = ref.match(/^#\/\$defs\/(.+)$/)?.[1]; if (!name || !root.$defs) return null; return root.$defs[name] ?? null; }; /** Fully resolve a schema, following `$ref` and unwrapping single-variant * oneOf/anyOf so we can inspect the concrete shape. */ const deepResolve = (schema: JsonSchema, root: JsonSchema): JsonSchema => { let s = schema; if (s.$ref) { const resolved = resolveRef(s.$ref, root); if (resolved) s = resolved; } if (s.oneOf?.length === 1) s = deepResolve(s.oneOf[0]!, root); if (s.anyOf?.length === 1) s = deepResolve(s.anyOf[0]!, root); return s; }; // --- Small schema utilities ------------------------------------------------ const asObjectSchema = (value: unknown): JsonSchema | undefined => value && typeof value === "object" && !Array.isArray(value) ? (value as JsonSchema) : undefined; const schemaTypes = (schema: JsonSchema): readonly string[] => Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []; /** The non-null primitive type for `type: ["string","null"]` / `nullable`. */ const primaryType = (schema: JsonSchema): string | undefined => schemaTypes(schema).find((t: string) => t !== "null"); const schemaDescription = (schema: JsonSchema): string | undefined => { const d = schema.description; if (typeof d === "string") return d; if (Option.isOption(d) && Option.isSome(d) && typeof d.value === "string") return d.value; return undefined; }; export const emptyEnumGuidance = (description: string | undefined): string => { const integration = description?.match(/^Connection to use for .+ \(([^)]+)\)$/)?.[1]; return integration ? `Connect ${integration} first.` : "No options available."; }; const DEFAULT_BY_TYPE: Record<string, () => unknown> = { string: () => "", number: () => 0, integer: () => 0, boolean: () => false, object: () => ({}), array: () => [], }; /** Seed used when the user clicks "Add" for an optional property or array item. * Schema defaults can also be displayed without being injected into JSON. */ export const defaultForSchema = (schema: JsonSchema, root: JsonSchema): unknown => { const s = deepResolve(schema, root); if (s.default !== undefined) return s.default; if (s.const !== undefined) return s.const; if (Array.isArray(s.enum) && s.enum.length > 0) return s.enum[0]; const t = primaryType(s); const make = t ? DEFAULT_BY_TYPE[t] : undefined; return make ? make() : null; }; /** True only when the schema resolves to a renderable object root (a `type: * "object"` with `properties`). Otherwise the Form tab is disabled and the * panel falls back to raw JSON. */ export const isRenderableObjectSchema = ( schema: unknown, definitions?: Record<string, unknown>, ): boolean => { const obj = asObjectSchema(schema); if (!obj) return false; const root: JsonSchema = { $defs: (definitions ?? {}) as Record<string, JsonSchema>, }; const resolved = deepResolve(obj, root); return resolved.type === "object" || resolved.properties !== undefined; }; /** Required top-level fields not yet satisfied by `value` — a field counts as * satisfied when it is present and not undefined/null/empty-string. Empty array * ⇒ all required args supplied; drives the Run button's enabled state. */ export const missingRequiredFields = ( schema: unknown, definitions: Record<string, unknown> | undefined, value: Record<string, unknown>, ): readonly string[] => { const obj = asObjectSchema(schema); if (!obj) return []; const root: JsonSchema = { $defs: (definitions ?? {}) as Record<string, JsonSchema> }; const resolved = deepResolve(obj, root); const required = Array.isArray(resolved.required) ? resolved.required : []; return required.filter((key: string): boolean => { if (!(key in value)) return true; const v = value[key]; return v === undefined || v === null || v === ""; }); }; // --- Pure value transforms (unit-tested directly) -------------------------- /** Rebuild an object with `key` set to `next` — never mutates the input, never * produces `undefined` values. */ export const setProperty = ( value: Record<string, unknown>, key: string, next: unknown, ): Record<string, unknown> => ({ ...value, [key]: next }); /** Rebuild an object with `key` deleted. */ export const removeProperty = ( value: Record<string, unknown>, key: string, ): Record<string, unknown> => { const { [key]: _omit, ...rest } = value; return rest; }; /** Apply a child field's emitted value to its object parent: the REMOVE sentinel * deletes the key, anything else sets it. */ const applyChild = ( value: Record<string, unknown>, key: string, next: unknown, ): Record<string, unknown> => next === REMOVE ? removeProperty(value, key) : setProperty(value, key, next); /** Coerce a number-input string to a JSON number, or `undefined` if not a finite * number (caller keeps last-valid + marks invalid). */ export const coerceNumber = (raw: string, integer: boolean): number | undefined => { if (raw.trim() === "") return undefined; const n = Number(raw); if (!Number.isFinite(n)) return undefined; return integer ? Math.trunc(n) : n; }; // --------------------------------------------------------------------------- // JsonSchemaForm — top-level object renderer // --------------------------------------------------------------------------- export function JsonSchemaForm(props: { readonly schema: unknown; readonly definitions?: Record<string, unknown>; readonly value: Record<string, unknown>; readonly onChange: (next: Record<string, unknown>) => void; readonly disabled?: boolean; }): React.ReactElement { const { schema, definitions, value, onChange, disabled } = props; const root = useMemo<JsonSchema>( () => ({ $defs: (definitions ?? {}) as Record<string, JsonSchema> }), [definitions], ); const resolved = useMemo<JsonSchema>( () => deepResolve(asObjectSchema(schema) ?? {}, root), [schema, root], ); return ( <ObjectFields schema={resolved} root={root} value={value} onChange={onChange} depth={0} disabled={disabled} /> ); } // --------------------------------------------------------------------------- // ObjectFields — renders an object's properties (required/default-backed first, // then present optionals, then "Add" rows for absent optionals) // --------------------------------------------------------------------------- function ObjectFields(props: { readonly schema: JsonSchema; readonly root: JsonSchema; readonly value: Record<string, unknown>; readonly onChange: (next: Record<string, unknown>) => void; readonly depth: number; readonly idPrefix?: string; readonly disabled?: boolean; }): React.ReactElement { const { schema, root, value, onChange, depth, idPrefix = "jsf", disabled } = props; const properties = schema.properties ?? {}; const requiredSet = new Set(schema.required ?? []); const entries = Object.entries(properties); const present = entries.filter(([key, propSchema]: [string, JsonSchema]) => { const resolved = deepResolve(propSchema, root); return key in value || requiredSet.has(key) || resolved.default !== undefined; }); const addable = entries.filter( ([key, propSchema]: [string, JsonSchema]) => !requiredSet.has(key) && !(key in value) && deepResolve(propSchema, root).default === undefined, ); // No properties → render nothing (no "takes no arguments" noise); a zero-arg // tool simply runs with an empty payload. if (entries.length === 0) { return <></>; } return ( <div className="space-y-3"> {present.map(([key, propSchema]: [string, JsonSchema]) => { const fieldId = `${idPrefix}-${key}`; return ( <JsonSchemaField key={key} schema={propSchema} root={root} name={key} fieldId={fieldId} required={requiredSet.has(key)} value={value[key]} present={key in value} onChange={(next: unknown) => onChange(applyChild(value, key, next))} depth={depth} disabled={disabled} /> ); })} {addable.length > 0 && ( <div className="flex flex-wrap gap-1.5 pt-1"> {addable.map(([key, propSchema]: [string, JsonSchema]) => ( <Button key={key} type="button" size="xs" variant="outline" disabled={disabled} onClick={() => onChange(setProperty(value, key, defaultForSchema(propSchema, root)))} > + {key} </Button> ))} </div> )} </div> ); } // --------------------------------------------------------------------------- // JsonSchemaField — dispatches one property to the right control // --------------------------------------------------------------------------- function JsonSchemaField(props: { readonly schema: JsonSchema; readonly root: JsonSchema; readonly name: string; readonly fieldId: string; readonly required: boolean; readonly value: unknown; /** Whether the key is present in the parent object (drives the Remove button). */ readonly present: boolean; readonly onChange: (next: unknown) => void; readonly depth: number; readonly disabled?: boolean; }): React.ReactElement { const { root, name, fieldId, required, value, present, onChange, depth, disabled } = props; const schema = deepResolve(props.schema, root); const description = schemaDescription(schema); const fieldValue = value === undefined && schema.default !== undefined ? schema.default : value; const header = ( <div className="flex items-center justify-between gap-2"> <Label htmlFor={fieldId} className="text-xs"> <span>{name}</span> {required && ( <Badge variant="secondary" className="px-1.5 py-0 text-[10px]"> required </Badge> )} </Label> {!required && present && ( <Button type="button" size="xs" variant="ghost" className="h-5 px-1.5 text-muted-foreground" disabled={disabled} onClick={() => onChange(REMOVE)} > Remove </Button> )} </div> ); const help = description ? ( <p className="text-[11px] leading-4 text-muted-foreground">{description}</p> ) : null; return ( <div className="space-y-1"> {header} <FieldControl schema={schema} root={root} fieldId={fieldId} value={fieldValue} onChange={onChange} depth={depth} disabled={disabled} /> {help} </div> ); } // --------------------------------------------------------------------------- // FieldControl — the actual control for a resolved schema node // --------------------------------------------------------------------------- function FieldControl(props: { readonly schema: JsonSchema; readonly root: JsonSchema; readonly fieldId: string; readonly value: unknown; readonly onChange: (next: unknown) => void; readonly depth: number; readonly disabled?: boolean; }): React.ReactElement { const { schema, root, fieldId, value, onChange, depth, disabled } = props; // Recursion guard / unsupported constructs → raw-JSON subtree fallback. const unsupported = depth > 8 || (schema.oneOf?.length ?? 0) > 1 || (schema.anyOf?.length ?? 0) > 1 || schema.allOf !== undefined || Array.isArray(schema.items) || (schema.$ref !== undefined && resolveRef(schema.$ref, root) === null); if (unsupported) { return ( <JsonFallbackField schema={schema} root={root} fieldId={fieldId} value={value} onChange={onChange} disabled={disabled} /> ); } // const — fixed value, shown disabled and auto-applied. if (schema.const !== undefined) { return ( <ConstField fieldId={fieldId} constValue={schema.const} value={value} onChange={onChange} /> ); } if (Array.isArray(schema.enum) && schema.enum.length === 0) { const guidance = emptyEnumGuidance(schemaDescription(schema)); return ( <div className="space-y-1"> <NativeSelect id={fieldId} className="w-full text-xs" value="" disabled={true}> <NativeSelectOption value="">{guidance}</NativeSelectOption> </NativeSelect> <p className="text-[11px] leading-4 text-muted-foreground">{guidance}</p> </div> ); } // enum — native select. Optional/unset gets a leading empty option. if (Array.isArray(schema.enum) && schema.enum.length > 0) { const options = schema.enum; const currentIndex = options.findIndex((o: unknown) => o === value); return ( <NativeSelect id={fieldId} className="w-full text-xs" value={currentIndex >= 0 ? String(currentIndex) : ""} disabled={disabled} onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { const idx = Number(e.target.value); if (Number.isInteger(idx) && idx >= 0 && idx < options.length) onChange(options[idx]); }} > <NativeSelectOption value="">— select —</NativeSelectOption> {options.map((option: unknown, i: number) => ( <NativeSelectOption key={i} value={String(i)}> {safeSchemaValueLabel(option)} </NativeSelectOption> ))} </NativeSelect> ); } const type = primaryType(schema); if (type === "boolean") { return ( <div className="flex h-9 items-center"> <Switch id={fieldId} checked={value === true} disabled={disabled} onCheckedChange={(checked: boolean) => onChange(checked)} /> </div> ); } if (type === "number" || type === "integer") { return ( <NumberField schema={schema} fieldId={fieldId} value={value} onChange={onChange} integer={type === "integer"} disabled={disabled} /> ); } if (type === "string") { const inputType = schema.format === "email" ? "email" : schema.format === "uri" || schema.format === "url" ? "url" : schema.format === "date" ? "date" : "text"; return ( <Input id={fieldId} type={inputType} className="text-xs" value={typeof value === "string" ? value : ""}