import { useState, useEffect } from "react"; import { Plus, Trash2, ChevronDown, ChevronRight, Code } from "lucide-react"; interface SchemaProperty { name: string; type: string; description: string; required: boolean; secret: boolean; default?: string; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; pattern?: string; enum?: string[]; } interface SchemaBuilderProps { value: Record; onChange: (schema: Record) => void; label?: string; placeholder?: string; error?: string; className?: string; disabled?: boolean; } const PROPERTY_TYPES = [ { value: "string", label: "String" }, { value: "number", label: "Number" }, { value: "integer", label: "Integer" }, { value: "boolean", label: "Boolean" }, { value: "array", label: "Array" }, { value: "object", label: "Object" }, ]; export default function SchemaBuilder({ value, onChange, label, placeholder, error, className = "", disabled = false, }: SchemaBuilderProps) { const [properties, setProperties] = useState([]); const [showRawJson, setShowRawJson] = useState(false); const [rawJson, setRawJson] = useState(""); const [rawJsonError, setRawJsonError] = useState(""); const [expandedProperties, setExpandedProperties] = useState>( new Set(), ); // Initialize properties from schema value // Expects StackStorm-style flat format: { param_name: { type, required, secret, ... }, ... } useEffect(() => { if (!value || typeof value !== "object") return; const props: SchemaProperty[] = []; Object.entries(value).forEach(([name, propDef]: [string, any]) => { if (propDef && typeof propDef === "object" && !Array.isArray(propDef)) { props.push({ name, type: propDef.type || "string", description: propDef.description || "", required: propDef.required === true, secret: propDef.secret === true, default: propDef.default !== undefined ? JSON.stringify(propDef.default) : undefined, minimum: propDef.minimum, maximum: propDef.maximum, minLength: propDef.minLength, maxLength: propDef.maxLength, pattern: propDef.pattern, enum: propDef.enum, }); } }); if (props.length > 0) { setProperties(props); } }, []); // Update raw JSON when switching to raw view useEffect(() => { if (showRawJson) { setRawJson(JSON.stringify(buildSchema(), null, 2)); setRawJsonError(""); } }, [showRawJson]); // Build StackStorm-style flat parameter schema const buildSchema = (): Record => { if (properties.length === 0) { return {}; } const schema: Record = {}; properties.forEach((prop) => { const propSchema: Record = { type: prop.type, }; if (prop.description) { propSchema.description = prop.description; } if (prop.required) { propSchema.required = true; } if (prop.secret) { propSchema.secret = true; } if (prop.default !== undefined && prop.default !== "") { try { propSchema.default = JSON.parse(prop.default); } catch { propSchema.default = prop.default; } } // Type-specific constraints if (prop.type === "string") { if (prop.minLength !== undefined) propSchema.minLength = prop.minLength; if (prop.maxLength !== undefined) propSchema.maxLength = prop.maxLength; if (prop.pattern) propSchema.pattern = prop.pattern; if (prop.enum && prop.enum.length > 0) propSchema.enum = prop.enum; } if (prop.type === "number" || prop.type === "integer") { if (prop.minimum !== undefined) propSchema.minimum = prop.minimum; if (prop.maximum !== undefined) propSchema.maximum = prop.maximum; } schema[prop.name] = propSchema; }); return schema; }; const handlePropertiesChange = (newProperties: SchemaProperty[]) => { setProperties(newProperties); const schema = buildSchemaFromProperties(newProperties); onChange(schema); }; // Build StackStorm-style flat parameter schema from properties array const buildSchemaFromProperties = ( props: SchemaProperty[], ): Record => { if (props.length === 0) { return {}; } const schema: Record = {}; props.forEach((prop) => { const propSchema: Record = { type: prop.type, }; if (prop.description) { propSchema.description = prop.description; } if (prop.required) { propSchema.required = true; } if (prop.secret) { propSchema.secret = true; } if (prop.default !== undefined && prop.default !== "") { try { propSchema.default = JSON.parse(prop.default); } catch { propSchema.default = prop.default; } } if (prop.type === "string") { if (prop.minLength !== undefined) propSchema.minLength = prop.minLength; if (prop.maxLength !== undefined) propSchema.maxLength = prop.maxLength; if (prop.pattern) propSchema.pattern = prop.pattern; if (prop.enum && prop.enum.length > 0) propSchema.enum = prop.enum; } if (prop.type === "number" || prop.type === "integer") { if (prop.minimum !== undefined) propSchema.minimum = prop.minimum; if (prop.maximum !== undefined) propSchema.maximum = prop.maximum; } schema[prop.name] = propSchema; }); return schema; }; const addProperty = () => { const newProp: SchemaProperty = { name: `param${properties.length + 1}`, type: "string", description: "", required: false, secret: false, }; const newIndex = properties.length; handlePropertiesChange([...properties, newProp]); setExpandedProperties(new Set([...expandedProperties, newIndex])); }; const removeProperty = (index: number) => { const newProperties = properties.filter((_, i) => i !== index); handlePropertiesChange(newProperties); // Update expanded indices: remove the deleted index and shift down higher indices const newExpanded = new Set(); expandedProperties.forEach((expandedIndex) => { if (expandedIndex < index) { newExpanded.add(expandedIndex); } else if (expandedIndex > index) { newExpanded.add(expandedIndex - 1); } // If expandedIndex === index, it's removed (not added to newExpanded) }); setExpandedProperties(newExpanded); }; const updateProperty = (index: number, updates: Partial) => { const newProperties = [...properties]; newProperties[index] = { ...newProperties[index], ...updates }; handlePropertiesChange(newProperties); }; const toggleExpanded = (index: number) => { const newExpanded = new Set(expandedProperties); if (newExpanded.has(index)) { newExpanded.delete(index); } else { newExpanded.add(index); } setExpandedProperties(newExpanded); }; const handleRawJsonChange = (newJson: string) => { setRawJson(newJson); setRawJsonError(""); try { const parsed = JSON.parse(newJson); if (typeof parsed !== "object" || Array.isArray(parsed)) { setRawJsonError("Schema must be a JSON object"); return; } onChange(parsed); // Update properties from parsed JSON // Expects StackStorm-style flat format: { param_name: { type, required, secret, ... }, ... } const props: SchemaProperty[] = []; Object.entries(parsed).forEach(([name, propDef]: [string, any]) => { if (propDef && typeof propDef === "object" && !Array.isArray(propDef)) { props.push({ name, type: propDef.type || "string", description: propDef.description || "", required: propDef.required === true, secret: propDef.secret === true, default: propDef.default !== undefined ? JSON.stringify(propDef.default) : undefined, minimum: propDef.minimum, maximum: propDef.maximum, minLength: propDef.minLength, maxLength: propDef.maxLength, pattern: propDef.pattern, enum: propDef.enum, }); } }); setProperties(props); } catch (e: any) { setRawJsonError(e.message); } }; return (
{label && ( )}
{/* Header with view toggle */}
{showRawJson ? "Raw JSON Schema" : "Schema Properties"} {disabled && ( Read-only )} {!disabled && ( )}
{/* Content */}
{showRawJson ? ( // Raw JSON editor