( schema: JsonSchema | undefined, initialValues?: Record<string, string>, )
| 36 | // ==================== Hook ==================== |
| 37 | |
| 38 | export function useSchemaForm( |
| 39 | schema: JsonSchema | undefined, |
| 40 | initialValues?: Record<string, string>, |
| 41 | ): UseSchemaFormResult { |
| 42 | // Parse schema into const values and editable field descriptors |
| 43 | const { constValues, fieldDefs, defaults, booleanKeys } = useMemo(() => { |
| 44 | const consts: Record<string, unknown> = {} |
| 45 | const fields: SchemaField[] = [] |
| 46 | const defs: Record<string, string> = {} |
| 47 | |
| 48 | const props = (schema?.properties ?? {}) as Record<string, JsonSchemaProperty> |
| 49 | const required = new Set((schema?.required as string[]) ?? []) |
| 50 | |
| 51 | for (const [key, prop] of Object.entries(props)) { |
| 52 | // const → hidden, value auto-merged |
| 53 | if (prop.const !== undefined) { |
| 54 | consts[key] = prop.const |
| 55 | continue |
| 56 | } |
| 57 | |
| 58 | const title = prop.title ?? key.charAt(0).toUpperCase() + key.slice(1) |
| 59 | const isRequired = required.has(key) |
| 60 | |
| 61 | // Determine field type |
| 62 | if (prop.writeOnly) { |
| 63 | fields.push({ key, type: 'password', title, description: prop.description, required: isRequired }) |
| 64 | } else if (prop.oneOf) { |
| 65 | const options = prop.oneOf.map(o => ({ value: o.const, label: o.title })) |
| 66 | fields.push({ key, type: 'select', title, description: prop.description, required: isRequired, options }) |
| 67 | } else if (prop.enum) { |
| 68 | const options = prop.enum.map(v => ({ value: v, label: v })) |
| 69 | fields.push({ key, type: 'select', title, description: prop.description, required: isRequired, options }) |
| 70 | } else if (prop.type === 'boolean') { |
| 71 | // Stored in string form-state as 'true'/'false'; getSubmitData |
| 72 | // converts back to a real boolean. Rendered as a checkbox. |
| 73 | fields.push({ key, type: 'boolean', title, description: prop.description, required: isRequired, defaultValue: String(prop.default ?? false) }) |
| 74 | } else { |
| 75 | fields.push({ key, type: 'text', title, description: prop.description, required: isRequired, defaultValue: prop.default !== undefined ? String(prop.default) : undefined }) |
| 76 | } |
| 77 | |
| 78 | // Collect defaults. Booleans always seed (a checkbox/toggle is never |
| 79 | // "unset"), so even a required boolean with no .default() lands in form |
| 80 | // state and submits a real `false` rather than going missing. |
| 81 | if (prop.type === 'boolean') { |
| 82 | defs[key] = String(prop.default ?? false) |
| 83 | } else if (prop.default !== undefined) { |
| 84 | defs[key] = String(prop.default) |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | const booleanKeys = new Set(fields.filter(f => f.type === 'boolean').map(f => f.key)) |
| 89 | return { constValues: consts, fieldDefs: fields, defaults: defs, booleanKeys } |
| 90 | }, [schema]) |
| 91 | |
| 92 | // Form state — reset when schema changes (e.g. user picks a different preset) |
| 93 | const [formData, setFormData] = useState<Record<string, string>>(() => ({ |
| 94 | ...defaults, |
| 95 | ...(initialValues ?? {}), |
no test coverage detected