Files
botpress--botpress/packages/common/src/oauth-wizard/schema-to-fields.ts
T
2026-07-13 13:34:48 +08:00

140 lines
3.4 KiB
TypeScript

import { z } from '@botpress/sdk'
export type HtmlInputType =
| 'text'
| 'number'
| 'email'
| 'password'
| 'url'
| 'date'
| 'time'
| 'color'
| 'checkbox'
| 'radio'
| 'select'
| 'textarea'
| 'hidden'
export type FormFieldDescriptor = {
name: string
label: string
inputType: HtmlInputType
displayAsParams: Record<string, unknown>
placeholder?: string
required: boolean
disabled: boolean
hidden: boolean
defaultValue?: string
options?: { label: string; value: string }[]
description?: string
error?: string
previousValue?: string
}
type ZuiMetadata = {
displayAs?: [string, Record<string, unknown>]
title?: string
placeholder?: string
hidden?: boolean | string
disabled?: boolean | string
}
const DISPLAY_AS_TO_HTML: Record<string, HtmlInputType> = {
switch: 'checkbox',
checkbox: 'checkbox',
radiogroup: 'radio',
dropdown: 'select',
select: 'select',
password: 'password',
secret: 'password',
number: 'number',
email: 'email',
url: 'url',
date: 'date',
time: 'time',
color: 'color',
hidden: 'hidden',
textarea: 'textarea',
}
function _resolveHtmlInputType(displayAsId: string | undefined, naked: z.ZodType, hasEnum: boolean): HtmlInputType {
if (displayAsId) {
const mapped = DISPLAY_AS_TO_HTML[displayAsId]
if (mapped) {
return mapped
}
}
if (hasEnum) {
return 'select'
}
if (z.is.zuiBoolean(naked)) {
return 'checkbox'
}
if (z.is.zuiNumber(naked)) {
return 'number'
}
return 'text'
}
function _capitalizeFieldName(name: string): string {
return name
.replace(/([A-Z])/g, ' $1')
.replace(/[_-]/g, ' ')
.replace(/^\w/, (c) => c.toUpperCase())
.trim()
}
function findDefault(field: z.ZodType): unknown {
let current: z.ZodType = field
while (current) {
if (z.is.zuiDefault(current)) {
return current._def.defaultValue()
}
if ('innerType' in current._def) {
current = (current._def as { innerType: z.ZodType }).innerType
} else {
break
}
}
return undefined
}
export function schemaToFieldDescriptors<T extends z.ZodObject>(
schema: T,
errors?: z.ZodError<z.input<T>>,
previousValues?: z.input<T>
): FormFieldDescriptor[] {
const fields: FormFieldDescriptor[] = []
for (const [name, field] of Object.entries(schema.shape) as [string, z.ZodType][]) {
const zui = field._def[z.zuiKey] as ZuiMetadata | undefined
const naked = field.naked()
const displayAs = zui?.displayAs
const displayAsId = displayAs?.[0]
const displayAsParams = displayAs?.[1] ?? {}
const isEnum = z.is.zuiEnum(naked)
const options = isEnum ? (naked._def.values as string[]).map((v) => ({ label: v, value: v })) : undefined
const inputType = _resolveHtmlInputType(displayAsId, naked, isEnum)
const defaultValue = findDefault(field)
fields.push({
name,
label: zui?.title ?? _capitalizeFieldName(name),
inputType,
displayAsParams,
placeholder: zui?.placeholder,
required: !field.isOptional(),
disabled: zui?.disabled === true,
hidden: zui?.hidden === true,
defaultValue: defaultValue != null ? String(defaultValue) : undefined,
options,
description: field._def.description as string | undefined,
error: errors?.issues.find((issue) => issue.path[0] === name)?.message,
previousValue: previousValues?.[name] != null ? String(previousValues[name]) : undefined,
})
}
return fields
}