feat: array, config label

This commit is contained in:
zernonia 2024-04-19 08:58:14 +08:00
parent 7370383fbe
commit 1120f044e2
17 changed files with 174 additions and 273 deletions

View File

@ -8,98 +8,19 @@ import { toast } from '@/lib/registry/new-york/ui/toast'
import type { Config } from '@/lib/registry/new-york/ui/auto-form' import type { Config } from '@/lib/registry/new-york/ui/auto-form'
import { AutoForm, AutoFormField } from '@/lib/registry/new-york/ui/auto-form' import { AutoForm, AutoFormField } from '@/lib/registry/new-york/ui/auto-form'
enum Sports {
Football = 'Football/Soccer',
Basketball = 'Basketball',
Baseball = 'Baseball',
Hockey = 'Hockey (Ice)',
None = 'I don\'t like sports',
}
const schema = z.object({ const schema = z.object({
username: z guestListName: z.string(),
.string({ invitedGuests: z
required_error: 'Username is required.', .array(
}) z.object({
.min(2, { name: z.string(),
message: 'Username must be at least 2 characters.', age: z.coerce.number(),
}),
password: z
.string({
required_error: 'Password is required.',
})
.describe('Your secure password')
.min(8, {
message: 'Password must be at least 8 characters.',
}),
favouriteNumber: z.coerce
.number({
invalid_type_error: 'Favourite number must be a number.',
})
.min(1, {
message: 'Favourite number must be at least 1.',
})
.max(10, {
message: 'Favourite number must be at most 10.',
})
.default(1)
.optional(),
acceptTerms: z
.boolean()
.describe('Accept terms and conditions.')
.refine(value => value, {
message: 'You must accept the terms and conditions.',
path: ['acceptTerms'],
}),
sendMeMails: z.boolean().optional(),
birthday: z.coerce.date().optional(),
color: z.enum(['red', 'green', 'blue']).optional(),
// Another enum example
marshmallows: z
.enum(['not many', 'a few', 'a lot', 'too many'])
.describe('How many marshmallows fit in your mouth?'),
// Native enum example
sports: z.nativeEnum(Sports).describe('What is your favourite sport?'),
bio: z
.string()
.min(10, {
message: 'Bio must be at least 10 characters.',
})
.max(160, {
message: 'Bio must not be longer than 30 characters.',
})
.optional(),
customParent: z.string().optional(),
file: z.string().optional().describe('Text file'),
subObject: z.object({
subField: z.string().optional().default('Sub Field'),
numberField: z.number().optional().default(1),
subSubObject: z
.object({
subSubField: z.string().default('Sub Sub Field'),
}), }),
}).describe('123 Description'), ).default([
{ name: '123', age: 30 },
optionalSubObject: z ]).describe('How many guests'),
.object({
optionalSubField: z.string(),
otherOptionalSubField: z.string(),
})
.optional(),
list: z.array(z.string()).describe('test the config'),
}) })
const formSchema = toTypedSchema(schema) const formSchema = toTypedSchema(schema)
@ -114,81 +35,30 @@ const onSubmit = handleSubmit((values) => {
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))), description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
}) })
}) })
const config
= reactive({
username: {
label: '123',
description: 'Test description',
inputProps: {
id: '123',
},
},
password: {
inputProps: {
id: '345',
type: 'password',
},
},
acceptTerms: {
description: 'this is custom',
},
sendMeMails: {
component: 'switch',
},
color: {
enumProps: {
options: ['red', 'green', 'blue'],
placeholder: 'Choose a color',
},
},
marshmallows: {
component: 'radio',
},
bio: {
component: 'textarea',
},
file: {
component: 'file',
},
subObject: {
subField: {
label: 'custom labvel',
description: '123',
},
subSubObject: {
subSubField: {
label: 'sub suuuub',
},
},
},
}) as Config<z.infer<typeof schema>>
</script> </script>
<template> <template>
<AutoForm <AutoForm
class="w-2/3 space-y-6" class="w-2/3 space-y-6"
:schema="schema" :schema="schema"
:field-config="config" :field-config="{
guestListName: {
label: 'Lisst',
inputProps: {
placeholder: 'testign123',
},
},
invitedGuests: {
name: {
label: 'walaaaaao',
},
},
list: {
label: 'woohooo',
},
}"
@submit="onSubmit" @submit="onSubmit"
> >
<template #username="componentField">
<div class="border p-4 rounded-lg shadow-sm">
<AutoFormField v-bind="{ ...componentField, name: 'username' }" />
</div>
</template>
<template #customParent="componentField">
<div class="flex items-end space-x-4">
<AutoFormField class="w-full" v-bind="{ ...componentField, name: 'customParent' }" />
<Button>Check</Button>
</div>
</template>
<template #subObject="componentField">
<AutoFormField v-bind="{ ...componentField, name: 'subObject' }" />
</template>
<Button type="submit"> <Button type="submit">
Submit Submit
</Button> </Button>

View File

@ -1,9 +1,10 @@
<script setup lang="ts" generic="U extends ZodRawShape, T extends ZodObject<U>"> <script setup lang="ts" generic="U extends ZodRawShape, T extends ZodObject<U>">
import { computed } from 'vue' import { computed } from 'vue'
import type { ZodAny, ZodObject, ZodRawShape, z } from 'zod' import type { ZodAny, ZodObject, ZodRawShape, z } from 'zod'
import { getBaseSchema, getBaseType, getDefaultValueInZodStack } from './utils' import { getBaseType, getDefaultValueInZodStack } from './utils'
import type { Config, ConfigItem, Shape } from './interface' import type { Config, ConfigItem, Shape } from './interface'
import AutoFormField from './AutoFormField.vue' import AutoFormField from './AutoFormField.vue'
import { Accordion } from '@/lib/registry/new-york/ui/accordion'
const props = defineProps<{ const props = defineProps<{
schema: T schema: T

View File

@ -6,6 +6,7 @@ import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from './constant'
defineProps<{ defineProps<{
name: string name: string
shape: Shape shape: Shape
label?: string
config?: ConfigItem | Config<U> config?: ConfigItem | Config<U>
}>() }>()
@ -18,6 +19,7 @@ function isValidConfig(config: any): config is ConfigItem {
<component <component
:is="isValidConfig(config) ? INPUT_COMPONENTS[config.component!] : INPUT_COMPONENTS[DEFAULT_ZOD_HANDLERS[shape.type]] " :is="isValidConfig(config) ? INPUT_COMPONENTS[config.component!] : INPUT_COMPONENTS[DEFAULT_ZOD_HANDLERS[shape.type]] "
:name="name" :name="name"
:label="label"
:required="shape.required" :required="shape.required"
:options="shape.options" :options="shape.options"
:schema="shape.schema" :schema="shape.schema"

View File

@ -1,6 +0,0 @@
<script setup lang="ts">
</script>
<template>
obkect
</template>

View File

@ -1,3 +1,4 @@
import FieldArray from './fields/Array.vue'
import FieldBoolean from './fields/Boolean.vue' import FieldBoolean from './fields/Boolean.vue'
import FieldDate from './fields/Date.vue' import FieldDate from './fields/Date.vue'
import FieldEnum from './fields/Enum.vue' import FieldEnum from './fields/Enum.vue'
@ -16,6 +17,7 @@ export const INPUT_COMPONENTS = {
number: FieldNumber, number: FieldNumber,
string: FieldInput, string: FieldInput,
file: FieldFile, file: FieldFile,
array: FieldArray,
object: FieldObject, object: FieldObject,
} }
@ -32,5 +34,6 @@ export const DEFAULT_ZOD_HANDLERS: {
ZodEnum: 'select', ZodEnum: 'select',
ZodNativeEnum: 'select', ZodNativeEnum: 'select',
ZodNumber: 'number', ZodNumber: 'number',
ZodArray: 'array',
ZodObject: 'object', ZodObject: 'object',
} }

View File

@ -0,0 +1,97 @@
<script setup lang="ts" generic="T extends z.ZodAny">
import * as z from 'zod'
import { computed } from 'vue'
import { PlusIcon, TrashIcon } from '@radix-icons/vue'
import { useFieldArray } from 'vee-validate'
import type { Config, ConfigItem } from '../interface'
import { beautifyObjectName, getBaseType } from '../utils'
import AutoFormField from '../AutoFormField.vue'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/new-york/ui/accordion'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Separator } from '@/lib/registry/new-york/ui/separator'
const props = defineProps<{
name: string
required?: boolean
config?: Config<T>
schema?: z.ZodArray<T>
}>()
const { remove, push, fields } = useFieldArray(props.name)
function isZodArray(
item: z.ZodArray<any> | z.ZodDefault<any>,
): item is z.ZodArray<any> {
return item instanceof z.ZodArray
}
function isZodDefault(
item: z.ZodArray<any> | z.ZodDefault<any>,
): item is z.ZodDefault<any> {
return item instanceof z.ZodDefault
}
const itemShape = computed(() => {
if (!props.schema)
return
const schema: z.ZodAny = isZodArray(props.schema)
? props.schema._def.type
: isZodDefault(props.schema)
// @ts-expect-error missing schema
? props.schema._def.innerType._def.type
: null
return {
type: getBaseType(schema),
schema,
}
})
</script>
<template>
<section>
<slot v-bind="props">
<Accordion type="multiple" class="w-full" collapsible>
<AccordionItem :value="name" class="border-none">
<AccordionTrigger class="text-base">
{{ schema?.description || beautifyObjectName(name) }}
</AccordionTrigger>
{{ fields }}
<AccordionContent class="p-2 space-y-5">
<template v-for="(field, index) of fields" :key="field.key">
<AutoFormField
:name="`${name}[${index}]`"
:label="name"
:shape="itemShape!"
:config="config as ConfigItem"
/>
<div class="!mt-2 flex justify-end">
<Button
type="button"
size="icon"
variant="secondary"
@click="remove(index)"
>
<TrashIcon />
</Button>
</div>
<Separator />
</template>
<Button
type="button"
variant="secondary"
class="mt-4 flex items-center"
@click="push(null)"
>
<PlusIcon class="mr-2" />
Add
</Button>
</AccordionContent>
</AccordionItem>
</Accordion>
</slot>
</section>
</template>

View File

@ -1,16 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { ConfigItem } from '../interface' import type { ConfigItem, FieldProps } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Switch } from '@/lib/registry/new-york/ui/switch' import { Switch } from '@/lib/registry/new-york/ui/switch'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox' import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
defineProps<{ defineProps<FieldProps>()
name: string
required?: boolean
config?: ConfigItem
}>()
</script> </script>
<template> <template>
@ -24,7 +20,7 @@ defineProps<{
</slot> </slot>
</FormControl> </FormControl>
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(label ?? name) }}
</AutoFormLabel> </AutoFormLabel>
</div> </div>

View File

@ -3,7 +3,7 @@ import { DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { CalendarIcon } from '@radix-icons/vue' import { CalendarIcon } from '@radix-icons/vue'
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import type { ConfigItem } from '../interface' import type { ConfigItem, FieldProps } from '../interface'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Calendar } from '@/lib/registry/new-york/ui/calendar' import { Calendar } from '@/lib/registry/new-york/ui/calendar'
@ -11,11 +11,7 @@ import { Button } from '@/lib/registry/new-york/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
defineProps<{ defineProps<FieldProps>()
name: string
required?: boolean
config?: ConfigItem
}>()
const df = new DateFormatter('en-US', { const df = new DateFormatter('en-US', {
dateStyle: 'long', dateStyle: 'long',
@ -26,7 +22,7 @@ const df = new DateFormatter('en-US', {
<FormField v-slot="slotProps" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem> <FormItem>
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(label ?? name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<slot v-bind="slotProps"> <slot v-bind="slotProps">

View File

@ -1,18 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { ConfigItem } from '../interface' import type { FieldProps } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/lib/registry/new-york/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/lib/registry/new-york/ui/select'
import { Label } from '@/lib/registry/new-york/ui/label' import { Label } from '@/lib/registry/new-york/ui/label'
import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group'
const props = defineProps<{ const props = defineProps<FieldProps & {
name: string
required?: boolean
options?: string[] options?: string[]
config?: ConfigItem
}>() }>()
const computedOptions = computed(() => props.config?.enumProps?.options || props.options) const computedOptions = computed(() => props.config?.enumProps?.options || props.options)
@ -22,7 +19,7 @@ const computedOptions = computed(() => props.config?.enumProps?.options || props
<FormField v-slot="slotProps" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem> <FormItem>
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(label ?? name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<slot v-bind="slotProps"> <slot v-bind="slotProps">

View File

@ -1,15 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { ConfigItem } from '../interface' import type { ConfigItem, FieldProps } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input' import { Input } from '@/lib/registry/new-york/ui/input'
defineProps<{ defineProps<FieldProps>()
name: string
required?: boolean
config?: ConfigItem
}>()
async function parseFileAsString(file: File | undefined): Promise<string> { async function parseFileAsString(file: File | undefined): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -31,7 +27,7 @@ async function parseFileAsString(file: File | undefined): Promise<string> {
<FormField v-slot="slotProps" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem v-bind="$attrs"> <FormItem v-bind="$attrs">
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(label ?? name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<slot v-bind="slotProps"> <slot v-bind="slotProps">

View File

@ -1,23 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { Config, ConfigItem } from '../interface' import type { Config, ConfigItem, FieldProps } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input' import { Input } from '@/lib/registry/new-york/ui/input'
import { Textarea } from '@/lib/registry/new-york/ui/textarea' import { Textarea } from '@/lib/registry/new-york/ui/textarea'
defineProps<{ defineProps<FieldProps>()
name: string
required?: boolean
config?: ConfigItem
}>()
</script> </script>
<template> <template>
<FormField v-slot="slotProps" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem v-bind="$attrs"> <FormItem v-bind="$attrs">
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(label ?? name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<slot v-bind="slotProps"> <slot v-bind="slotProps">

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { beautifyObjectName } from '../utils' import { beautifyObjectName } from '../utils'
import type { Config, ConfigItem } from '../interface' import type { Config, ConfigItem, FieldProps } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue' import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input' import { Input } from '@/lib/registry/new-york/ui/input'
@ -9,18 +9,14 @@ defineOptions({
inheritAttrs: false, inheritAttrs: false,
}) })
defineProps<{ defineProps<FieldProps>()
name: string
required?: boolean
config?: ConfigItem
}>()
</script> </script>
<template> <template>
<FormField v-slot="slotProps" :name="name"> <FormField v-slot="slotProps" :name="name">
<FormItem> <FormItem>
<AutoFormLabel v-if="!config?.hideLabel" :required="required"> <AutoFormLabel v-if="!config?.hideLabel" :required="required">
{{ config?.label || beautifyObjectName(name) }} {{ config?.label || beautifyObjectName(label ?? name) }}
</AutoFormLabel> </AutoFormLabel>
<FormControl> <FormControl>
<slot v-bind="slotProps"> <slot v-bind="slotProps">

View File

@ -1,7 +1,7 @@
<script setup lang="ts" generic="T extends ZodRawShape"> <script setup lang="ts" generic="T extends ZodRawShape">
import type { ZodAny, ZodObject, ZodRawShape } from 'zod' import type { ZodAny, ZodObject, ZodRawShape } from 'zod'
import { computed } from 'vue' import { computed } from 'vue'
import type { Config, ConfigItem } from '../interface' import type { Config, ConfigItem, Shape } from '../interface'
import { beautifyObjectName, getBaseSchema, getBaseType, getDefaultValueInZodStack } from '../utils' import { beautifyObjectName, getBaseSchema, getBaseType, getDefaultValueInZodStack } from '../utils'
import AutoFormField from '../AutoFormField.vue' import AutoFormField from '../AutoFormField.vue'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/new-york/ui/accordion' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/new-york/ui/accordion'
@ -15,15 +15,7 @@ const props = defineProps<{
const shapes = computed(() => { const shapes = computed(() => {
// @ts-expect-error ignore {} not assignable to object // @ts-expect-error ignore {} not assignable to object
const val: { const val: { [key in keyof T]: Shape } = {}
[key in keyof T]: {
type: string
default: any
required?: boolean
options?: string[]
schema?: ZodAny
}
} = {}
if (!props.schema) if (!props.schema)
return return
@ -60,7 +52,8 @@ const shapes = computed(() => {
<template v-for="(shape, key) in shapes" :key="key"> <template v-for="(shape, key) in shapes" :key="key">
<AutoFormField <AutoFormField
:config="config?.[key as keyof typeof config] as ConfigItem" :config="config?.[key as keyof typeof config] as ConfigItem"
:name="key.toString()" :name="`${name}${key.toString()}`"
:label="key.toString()"
:shape="shape" :shape="shape"
/> />
</template> </template>

View File

@ -1,3 +1,4 @@
export { default as AutoFormFieldArray } from './Array.vue'
export { default as AutoFormFieldBoolean } from './Boolean.vue' export { default as AutoFormFieldBoolean } from './Boolean.vue'
export { default as AutoFormFieldDate } from './Date.vue' export { default as AutoFormFieldDate } from './Date.vue'
export { default as AutoFormFieldEnum } from './Enum.vue' export { default as AutoFormFieldEnum } from './Enum.vue'

View File

@ -1,6 +1,5 @@
export { default as AutoForm } from './AutoForm.vue' export { default as AutoForm } from './AutoForm.vue'
export { default as AutoFormField } from './AutoFormField.vue' export { default as AutoFormField } from './AutoFormField.vue'
export { getObjectFormSchema, getBaseSchema, getBaseType, getDefaultValues } from './utils' export { getObjectFormSchema, getBaseSchema, getBaseType } from './utils'
export type { Config, ConfigItem } from './interface' export type { Config, ConfigItem } from './interface'
export * from './fields' export * from './fields'

View File

@ -2,9 +2,16 @@ import type { InputHTMLAttributes, SelectHTMLAttributes } from 'vue'
import type { ZodAny, z } from 'zod' import type { ZodAny, z } from 'zod'
import type { INPUT_COMPONENTS } from './constant' import type { INPUT_COMPONENTS } from './constant'
export interface FieldProps {
name: string
label?: string
required?: boolean
config?: ConfigItem
}
export interface Shape { export interface Shape {
type: string type: string
default: any default?: any
required?: boolean required?: boolean
options?: string[] options?: string[]
schema?: ZodAny schema?: ZodAny
@ -23,11 +30,17 @@ export interface ConfigItem {
enumProps?: SelectHTMLAttributes & { options?: any[] } enumProps?: SelectHTMLAttributes & { options?: any[] }
} }
// Define a type to unwrap an array
type UnwrapArray<T> = T extends (infer U)[] ? U : never
export type Config<SchemaType extends object> = { export type Config<SchemaType extends object> = {
// If SchemaType.key is an object, create a nested Config, otherwise ConfigItem // If SchemaType.key is an object, create a nested Config, otherwise ConfigItem
[Key in keyof SchemaType]?: SchemaType[Key] extends object [Key in keyof SchemaType]?:
? Config<SchemaType[Key]> SchemaType[Key] extends any[]
: ConfigItem; ? UnwrapArray<Config<SchemaType[Key]>>
: SchemaType[Key] extends object
? Config<SchemaType[Key]>
: ConfigItem;
} }
export enum DependencyType { export enum DependencyType {

View File

@ -1,5 +1,4 @@
import { ZodFirstPartyTypeKind, type z } from 'zod' import type { z } from 'zod'
import type { FieldConfig } from './interface'
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions. // TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
export type ZodObjectOrWrapped = export type ZodObjectOrWrapped =
@ -11,8 +10,9 @@ export type ZodObjectOrWrapped =
* e.g. "myString" -> "My String" * e.g. "myString" -> "My String"
*/ */
export function beautifyObjectName(string: string) { export function beautifyObjectName(string: string) {
// Remove bracketed indices
// if numbers only return the string // if numbers only return the string
let output = string.replace(/([A-Z])/g, ' $1') let output = string.replace(/\[\d+\]/g, '').replace(/([A-Z])/g, ' $1')
output = output.charAt(0).toUpperCase() + output.slice(1) output = output.charAt(0).toUpperCase() + output.slice(1)
return output return output
} }
@ -69,55 +69,6 @@ export function getDefaultValueInZodStack(schema: z.ZodAny): any {
return undefined return undefined
} }
/**
* Get all default values from a Zod schema.
*/
export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
schema: Schema,
fieldConfig?: FieldConfig<z.infer<Schema>>,
) {
if (!schema)
return null
const { shape } = schema
type DefaultValuesType = Partial<z.infer<Schema>>
const defaultValues = {} as DefaultValuesType
if (!shape)
return defaultValues
for (const key of Object.keys(shape)) {
const item = shape[key] as z.ZodAny
// @ts-expect-error weird
if (getBaseType(item) === ZodFirstPartyTypeKind.ZodObject) {
const defaultItems = getDefaultValues(
getBaseSchema(item) as unknown as z.ZodObject<any, any>,
fieldConfig?.[key] as FieldConfig<z.infer<Schema>>,
)
if (defaultItems !== null) {
for (const defaultItemKey of Object.keys(defaultItems)) {
const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType
defaultValues[pathKey] = defaultItems[defaultItemKey]
}
}
}
else {
let defaultValue = getDefaultValueInZodStack(item)
if (
(defaultValue === null || defaultValue === '')
&& fieldConfig?.[key]?.inputProps
) {
defaultValue = (fieldConfig?.[key]?.inputProps as unknown as any)
.defaultValue
}
if (defaultValue !== undefined)
defaultValues[key as keyof DefaultValuesType] = defaultValue
}
}
return defaultValues
}
export function getObjectFormSchema( export function getObjectFormSchema(
schema: ZodObjectOrWrapped, schema: ZodObjectOrWrapped,
): z.ZodObject<any, any> { ): z.ZodObject<any, any> {