140 lines
3.4 KiB
TypeScript
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
|
|
}
|