Revert "feat: Implement new auto-form components and fields"
This reverts commit dc17e4a8f755ce045b461858ffcd56f567c5658f.
This commit is contained in:
parent
dfa443d373
commit
882729ffc9
@ -1,23 +0,0 @@
|
|||||||
import { FormLabel } from "@/components/ui/form";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function AutoFormLabel({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
isRequired: boolean;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormLabel className={cn(className)}>
|
|
||||||
{label}
|
|
||||||
{isRequired && <span className="text-destructive"> *</span>}
|
|
||||||
</FormLabel>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoFormLabel;
|
|
@ -1,13 +0,0 @@
|
|||||||
function AutoFormTooltip({ fieldConfigItem }: { fieldConfigItem: any }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{fieldConfigItem?.description && (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-white">
|
|
||||||
{fieldConfigItem.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoFormTooltip;
|
|
@ -1,35 +0,0 @@
|
|||||||
import AutoFormCheckbox from "./fields/checkbox";
|
|
||||||
import AutoFormDate from "./fields/date";
|
|
||||||
import AutoFormEnum from "./fields/enum";
|
|
||||||
import AutoFormFile from "./fields/file";
|
|
||||||
import AutoFormInput from "./fields/input";
|
|
||||||
import AutoFormNumber from "./fields/number";
|
|
||||||
import AutoFormRadioGroup from "./fields/radio-group";
|
|
||||||
import AutoFormSwitch from "./fields/switch";
|
|
||||||
import AutoFormTextarea from "./fields/textarea";
|
|
||||||
|
|
||||||
export const INPUT_COMPONENTS = {
|
|
||||||
checkbox: AutoFormCheckbox,
|
|
||||||
date: AutoFormDate,
|
|
||||||
select: AutoFormEnum,
|
|
||||||
radio: AutoFormRadioGroup,
|
|
||||||
switch: AutoFormSwitch,
|
|
||||||
textarea: AutoFormTextarea,
|
|
||||||
number: AutoFormNumber,
|
|
||||||
file: AutoFormFile,
|
|
||||||
fallback: AutoFormInput,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define handlers for specific Zod types.
|
|
||||||
* You can expand this object to support more types.
|
|
||||||
*/
|
|
||||||
export const DEFAULT_ZOD_HANDLERS: {
|
|
||||||
[key: string]: keyof typeof INPUT_COMPONENTS;
|
|
||||||
} = {
|
|
||||||
ZodBoolean: "checkbox",
|
|
||||||
ZodDate: "date",
|
|
||||||
ZodEnum: "select",
|
|
||||||
ZodNativeEnum: "select",
|
|
||||||
ZodNumber: "number",
|
|
||||||
};
|
|
@ -1,57 +0,0 @@
|
|||||||
import { FieldValues, UseFormWatch } from "react-hook-form";
|
|
||||||
import { Dependency, DependencyType, EnumValues } from "./types";
|
|
||||||
import * as z from "zod";
|
|
||||||
|
|
||||||
export default function resolveDependencies<
|
|
||||||
SchemaType extends z.infer<z.ZodObject<any, any>>,
|
|
||||||
>(
|
|
||||||
dependencies: Dependency<SchemaType>[],
|
|
||||||
currentFieldName: keyof SchemaType,
|
|
||||||
watch: UseFormWatch<FieldValues>,
|
|
||||||
) {
|
|
||||||
let isDisabled = false;
|
|
||||||
let isHidden = false;
|
|
||||||
let isRequired = false;
|
|
||||||
let overrideOptions: EnumValues | undefined;
|
|
||||||
|
|
||||||
const currentFieldValue = watch(currentFieldName as string);
|
|
||||||
|
|
||||||
const currentFieldDependencies = dependencies.filter(
|
|
||||||
(dependency) => dependency.targetField === currentFieldName,
|
|
||||||
);
|
|
||||||
for (const dependency of currentFieldDependencies) {
|
|
||||||
const watchedValue = watch(dependency.sourceField as string);
|
|
||||||
|
|
||||||
const conditionMet = dependency.when(watchedValue, currentFieldValue);
|
|
||||||
|
|
||||||
switch (dependency.type) {
|
|
||||||
case DependencyType.DISABLES:
|
|
||||||
if (conditionMet) {
|
|
||||||
isDisabled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DependencyType.REQUIRES:
|
|
||||||
if (conditionMet) {
|
|
||||||
isRequired = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DependencyType.HIDES:
|
|
||||||
if (conditionMet) {
|
|
||||||
isHidden = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DependencyType.SETS_OPTIONS:
|
|
||||||
if (conditionMet) {
|
|
||||||
overrideOptions = dependency.options;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDisabled,
|
|
||||||
isHidden,
|
|
||||||
isRequired,
|
|
||||||
overrideOptions,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
import {
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { Plus, Trash } from "lucide-react";
|
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { beautifyObjectName } from "../utils";
|
|
||||||
import AutoFormObject from "./object";
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AutoFormArray({
|
|
||||||
name,
|
|
||||||
item,
|
|
||||||
form,
|
|
||||||
path = [],
|
|
||||||
fieldConfig,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
item: z.ZodArray<any> | z.ZodDefault<any>;
|
|
||||||
form: ReturnType<typeof useForm>;
|
|
||||||
path?: string[];
|
|
||||||
fieldConfig?: any;
|
|
||||||
}) {
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
|
||||||
control: form.control,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
const title = item._def.description ?? beautifyObjectName(name);
|
|
||||||
|
|
||||||
const itemDefType = isZodArray(item)
|
|
||||||
? item._def.type
|
|
||||||
: isZodDefault(item)
|
|
||||||
? item._def.innerType._def.type
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccordionItem value={name} className="border-none">
|
|
||||||
<AccordionTrigger>{title}</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
{fields.map((_field, index) => {
|
|
||||||
const key = _field.id;
|
|
||||||
return (
|
|
||||||
<div className="mt-4 flex flex-col" key={`${key}`}>
|
|
||||||
<AutoFormObject
|
|
||||||
schema={itemDefType as z.ZodObject<any, any>}
|
|
||||||
form={form}
|
|
||||||
fieldConfig={fieldConfig}
|
|
||||||
path={[...path, index.toString()]}
|
|
||||||
/>
|
|
||||||
<div className="my-4 flex justify-end">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="icon"
|
|
||||||
type="button"
|
|
||||||
className="hover:bg-zinc-300 hover:text-black focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-white dark:text-black dark:hover:bg-zinc-300 dark:hover:text-black dark:hover:ring-0 dark:hover:ring-offset-0 dark:focus-visible:ring-0 dark:focus-visible:ring-offset-0"
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
>
|
|
||||||
<Trash className="size-4 " />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => append({})}
|
|
||||||
className="mt-4 flex items-center"
|
|
||||||
>
|
|
||||||
<Plus className="mr-2" size={16} />
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { FormControl, FormItem } from "@/components/ui/form";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
|
|
||||||
export default function AutoFormCheckbox({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormItem>
|
|
||||||
<div className="mb-3 flex items-center gap-3">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
{...fieldProps}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
import { DatePicker } from "@/components/ui/date-picker";
|
|
||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormDate({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
<FormControl>
|
|
||||||
<DatePicker
|
|
||||||
date={field.value}
|
|
||||||
setDate={field.onChange}
|
|
||||||
{...fieldProps}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import * as z from "zod";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
import { getBaseSchema } from "../utils";
|
|
||||||
|
|
||||||
export default function AutoFormEnum({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
fieldConfigItem,
|
|
||||||
zodItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def
|
|
||||||
.values;
|
|
||||||
|
|
||||||
let values: [string, string][] = [];
|
|
||||||
if (!Array.isArray(baseValues)) {
|
|
||||||
values = Object.entries(baseValues);
|
|
||||||
} else {
|
|
||||||
values = baseValues.map((value) => [value, value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findItem(value: any) {
|
|
||||||
return values.find((item) => item[0] === value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...fieldProps}
|
|
||||||
>
|
|
||||||
<SelectTrigger className={fieldProps.className}>
|
|
||||||
<SelectValue placeholder={fieldConfigItem.inputProps?.placeholder}>
|
|
||||||
{field.value ? findItem(field.value)?.[1] : "Select an option"}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{values.map(([value, label]) => (
|
|
||||||
<SelectItem value={label} key={value}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Trash2 } from "lucide-react";
|
|
||||||
import { ChangeEvent, useState } from "react";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
export default function AutoFormFile({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
field,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
|
|
||||||
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
||||||
const [file, setFile] = useState<string | null>(null);
|
|
||||||
const [fileName, setFileName] = useState<string | null>(null);
|
|
||||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
setFile(reader.result as string);
|
|
||||||
setFileName(file.name);
|
|
||||||
field.onChange(reader.result as string);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveClick = () => {
|
|
||||||
setFile(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
{showLabel && (
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!file && (
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
{...fieldPropsWithoutShowLabel}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
value={""}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
{file && (
|
|
||||||
<div className="flex h-[40px] w-full flex-row items-center justify-between space-x-2 rounded-sm border p-2 text-black focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-white dark:text-black dark:focus-visible:ring-0 dark:focus-visible:ring-offset-0">
|
|
||||||
<p>{fileName}</p>
|
|
||||||
<button onClick={handleRemoveClick} aria-label="Remove image">
|
|
||||||
<Trash2 size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormInput({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
|
|
||||||
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
||||||
const type = fieldProps.type || "text";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row items-center space-x-2">
|
|
||||||
<FormItem className="flex w-full flex-col justify-start">
|
|
||||||
{showLabel && (
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FormControl>
|
|
||||||
<Input type={type} {...fieldPropsWithoutShowLabel} />
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormNumber({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
|
|
||||||
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
{showLabel && (
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FormControl>
|
|
||||||
<Input type="number" {...fieldPropsWithoutShowLabel} />
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,183 +0,0 @@
|
|||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { FormField } from "@/components/ui/form";
|
|
||||||
import { useForm, useFormContext } from "react-hook-form";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from "../config";
|
|
||||||
import { Dependency, FieldConfig, FieldConfigItem } from "../types";
|
|
||||||
import {
|
|
||||||
beautifyObjectName,
|
|
||||||
getBaseSchema,
|
|
||||||
getBaseType,
|
|
||||||
zodToHtmlInputProps,
|
|
||||||
} from "../utils";
|
|
||||||
import AutoFormArray from "./array";
|
|
||||||
import resolveDependencies from "../dependencies";
|
|
||||||
|
|
||||||
function DefaultParent({ children }: { children: React.ReactNode }) {
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AutoFormObject<
|
|
||||||
SchemaType extends z.ZodObject<any, any>,
|
|
||||||
>({
|
|
||||||
schema,
|
|
||||||
form,
|
|
||||||
fieldConfig,
|
|
||||||
path = [],
|
|
||||||
dependencies = [],
|
|
||||||
}: {
|
|
||||||
schema: SchemaType | z.ZodEffects<SchemaType>;
|
|
||||||
form: ReturnType<typeof useForm>;
|
|
||||||
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
|
|
||||||
path?: string[];
|
|
||||||
dependencies?: Dependency<z.infer<SchemaType>>[];
|
|
||||||
}) {
|
|
||||||
const { watch } = useFormContext(); // Use useFormContext to access the watch function
|
|
||||||
|
|
||||||
if (!schema) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { shape } = getBaseSchema<SchemaType>(schema) || {};
|
|
||||||
|
|
||||||
if (!shape) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleIfZodNumber = (item: z.ZodAny) => {
|
|
||||||
const isZodNumber = (item as any)._def.typeName === "ZodNumber";
|
|
||||||
const isInnerZodNumber =
|
|
||||||
(item._def as any).innerType?._def?.typeName === "ZodNumber";
|
|
||||||
|
|
||||||
if (isZodNumber) {
|
|
||||||
(item as any)._def.coerce = true;
|
|
||||||
} else if (isInnerZodNumber) {
|
|
||||||
(item._def as any).innerType._def.coerce = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion type="multiple" className="space-y-5 border-none">
|
|
||||||
{Object.keys(shape).map((name) => {
|
|
||||||
let item = shape[name] as z.ZodAny;
|
|
||||||
item = handleIfZodNumber(item) as z.ZodAny;
|
|
||||||
const zodBaseType = getBaseType(item);
|
|
||||||
const itemName = item._def.description ?? beautifyObjectName(name);
|
|
||||||
const key = [...path, name].join(".");
|
|
||||||
|
|
||||||
const {
|
|
||||||
isHidden,
|
|
||||||
isDisabled,
|
|
||||||
isRequired: isRequiredByDependency,
|
|
||||||
overrideOptions,
|
|
||||||
} = resolveDependencies(dependencies, name, watch);
|
|
||||||
if (isHidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zodBaseType === "ZodObject") {
|
|
||||||
return (
|
|
||||||
<AccordionItem value={name} key={key} className="border-none">
|
|
||||||
<AccordionTrigger>{itemName}</AccordionTrigger>
|
|
||||||
<AccordionContent className="p-2">
|
|
||||||
<AutoFormObject
|
|
||||||
schema={item as unknown as z.ZodObject<any, any>}
|
|
||||||
form={form}
|
|
||||||
fieldConfig={
|
|
||||||
(fieldConfig?.[name] ?? {}) as FieldConfig<
|
|
||||||
z.infer<typeof item>
|
|
||||||
>
|
|
||||||
}
|
|
||||||
path={[...path, name]}
|
|
||||||
/>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (zodBaseType === "ZodArray") {
|
|
||||||
return (
|
|
||||||
<AutoFormArray
|
|
||||||
key={key}
|
|
||||||
name={name}
|
|
||||||
item={item as unknown as z.ZodArray<any>}
|
|
||||||
form={form}
|
|
||||||
fieldConfig={fieldConfig?.[name] ?? {}}
|
|
||||||
path={[...path, name]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldConfigItem: FieldConfigItem = fieldConfig?.[name] ?? {};
|
|
||||||
const zodInputProps = zodToHtmlInputProps(item);
|
|
||||||
const isRequired =
|
|
||||||
isRequiredByDependency ||
|
|
||||||
zodInputProps.required ||
|
|
||||||
fieldConfigItem.inputProps?.required ||
|
|
||||||
false;
|
|
||||||
|
|
||||||
if (overrideOptions) {
|
|
||||||
item = z.enum(overrideOptions) as unknown as z.ZodAny;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={key}
|
|
||||||
key={key}
|
|
||||||
render={({ field }) => {
|
|
||||||
const inputType =
|
|
||||||
fieldConfigItem.fieldType ??
|
|
||||||
DEFAULT_ZOD_HANDLERS[zodBaseType] ??
|
|
||||||
"fallback";
|
|
||||||
|
|
||||||
const InputComponent =
|
|
||||||
typeof inputType === "function"
|
|
||||||
? inputType
|
|
||||||
: INPUT_COMPONENTS[inputType];
|
|
||||||
|
|
||||||
const ParentElement =
|
|
||||||
fieldConfigItem.renderParent ?? DefaultParent;
|
|
||||||
|
|
||||||
const defaultValue = fieldConfigItem.inputProps?.defaultValue;
|
|
||||||
const value = field.value ?? defaultValue ?? "";
|
|
||||||
|
|
||||||
const fieldProps = {
|
|
||||||
...zodToHtmlInputProps(item),
|
|
||||||
...field,
|
|
||||||
...fieldConfigItem.inputProps,
|
|
||||||
disabled: fieldConfigItem.inputProps?.disabled || isDisabled,
|
|
||||||
ref: undefined,
|
|
||||||
value: value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (InputComponent === undefined) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ParentElement key={`${key}.parent`}>
|
|
||||||
<InputComponent
|
|
||||||
zodInputProps={zodInputProps}
|
|
||||||
field={field}
|
|
||||||
fieldConfigItem={fieldConfigItem}
|
|
||||||
label={itemName}
|
|
||||||
isRequired={isRequired}
|
|
||||||
zodItem={item}
|
|
||||||
fieldProps={fieldProps}
|
|
||||||
className={fieldProps.className}
|
|
||||||
/>
|
|
||||||
</ParentElement>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import {
|
|
||||||
FormControl,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import * as z from "zod";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
import { getBaseSchema } from "../utils";
|
|
||||||
|
|
||||||
export default function AutoFormRadioGroup({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
zodItem,
|
|
||||||
fieldProps,
|
|
||||||
fieldConfigItem,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def
|
|
||||||
.values;
|
|
||||||
|
|
||||||
let values: string[] = [];
|
|
||||||
if (!Array.isArray(baseValues)) {
|
|
||||||
values = Object.entries(baseValues).map((item) => item[0]);
|
|
||||||
} else {
|
|
||||||
values = baseValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormItem>
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...fieldProps}
|
|
||||||
>
|
|
||||||
{values?.map((value: any) => (
|
|
||||||
<FormItem
|
|
||||||
key={value}
|
|
||||||
className="mb-2 flex items-center gap-3 space-y-0"
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value={value} />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">{value}</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { FormControl, FormItem } from "@/components/ui/form";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormSwitch({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
{...fieldProps}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormTextarea({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
|
|
||||||
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
{showLabel && (
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FormControl>
|
|
||||||
<Textarea {...fieldPropsWithoutShowLabel} />
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Form } from "@/components/ui/form";
|
|
||||||
import React from "react";
|
|
||||||
import { DefaultValues, FormState, useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
|
|
||||||
import AutoFormObject from "./fields/object";
|
|
||||||
import { Dependency, FieldConfig } from "./types";
|
|
||||||
import {
|
|
||||||
ZodObjectOrWrapped,
|
|
||||||
getDefaultValues,
|
|
||||||
getObjectFormSchema,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
export function AutoFormSubmit({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Button type="submit" disabled={disabled} className={className}>
|
|
||||||
{children ?? "Submit"}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AutoForm<SchemaType extends ZodObjectOrWrapped>({
|
|
||||||
formSchema,
|
|
||||||
values: valuesProp,
|
|
||||||
onValuesChange: onValuesChangeProp,
|
|
||||||
onParsedValuesChange,
|
|
||||||
onSubmit: onSubmitProp,
|
|
||||||
fieldConfig,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
dependencies,
|
|
||||||
}: {
|
|
||||||
formSchema: SchemaType;
|
|
||||||
values?: Partial<z.infer<SchemaType>>;
|
|
||||||
onValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
|
|
||||||
onParsedValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
|
|
||||||
onSubmit?: (values: z.infer<SchemaType>) => void;
|
|
||||||
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
|
|
||||||
children?:
|
|
||||||
| React.ReactNode
|
|
||||||
| ((formState: FormState<z.infer<SchemaType>>) => React.ReactNode);
|
|
||||||
className?: string;
|
|
||||||
dependencies?: Dependency<z.infer<SchemaType>>[];
|
|
||||||
}) {
|
|
||||||
const objectFormSchema = getObjectFormSchema(formSchema);
|
|
||||||
const defaultValues: DefaultValues<z.infer<typeof objectFormSchema>> | null =
|
|
||||||
getDefaultValues(objectFormSchema, fieldConfig);
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof objectFormSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: defaultValues ?? undefined,
|
|
||||||
values: valuesProp,
|
|
||||||
});
|
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
|
||||||
const parsedValues = formSchema.safeParse(values);
|
|
||||||
if (parsedValues.success) {
|
|
||||||
onSubmitProp?.(parsedValues.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = form.watch();
|
|
||||||
// valuesString is needed because form.watch() returns a new object every time
|
|
||||||
const valuesString = JSON.stringify(values);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
onValuesChangeProp?.(values);
|
|
||||||
const parsedValues = formSchema.safeParse(values);
|
|
||||||
if (parsedValues.success) {
|
|
||||||
onParsedValuesChange?.(parsedValues.data);
|
|
||||||
}
|
|
||||||
}, [valuesString]);
|
|
||||||
|
|
||||||
const renderChildren =
|
|
||||||
typeof children === "function"
|
|
||||||
? children(form.formState as FormState<z.infer<SchemaType>>)
|
|
||||||
: children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
form.handleSubmit(onSubmit)(e);
|
|
||||||
}}
|
|
||||||
className={cn("space-y-5", className)}
|
|
||||||
>
|
|
||||||
<AutoFormObject
|
|
||||||
schema={objectFormSchema}
|
|
||||||
form={form}
|
|
||||||
dependencies={dependencies}
|
|
||||||
fieldConfig={fieldConfig}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{renderChildren}
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoForm;
|
|
@ -1,244 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import AutoForm from "../index";
|
|
||||||
|
|
||||||
describe("<AutoForm />", () => {
|
|
||||||
it("renders fields", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string().min(2, {
|
|
||||||
message: "Username must be at least 2 characters.",
|
|
||||||
}),
|
|
||||||
|
|
||||||
password: z.string().describe("Your secure password").min(8, {
|
|
||||||
message: "Password must be at least 8 characters.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(<AutoForm formSchema={formSchema} />);
|
|
||||||
cy.get("input[name=username]").should("exist");
|
|
||||||
cy.get("input[name=password]").should("exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders fields with custom labels", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string().describe("Your username"),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(<AutoForm formSchema={formSchema} />);
|
|
||||||
|
|
||||||
cy.get("label").contains("Your username");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("generates default labels", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
someFieldName: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(<AutoForm formSchema={formSchema} />);
|
|
||||||
|
|
||||||
cy.get("label").contains("Some Field Name");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows setting custom field labels", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
someFieldName: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(
|
|
||||||
<AutoForm
|
|
||||||
fieldConfig={{
|
|
||||||
someFieldName: {
|
|
||||||
label: "My field name",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
formSchema={formSchema}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("label").contains("My field name");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows setting custom field props", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(
|
|
||||||
<AutoForm
|
|
||||||
formSchema={formSchema}
|
|
||||||
fieldConfig={{
|
|
||||||
username: {
|
|
||||||
inputProps: {
|
|
||||||
placeholder: "Enter your username",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("input[name=username]").should(
|
|
||||||
"have.attr",
|
|
||||||
"placeholder",
|
|
||||||
"Enter your username",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows setting custom field type", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(
|
|
||||||
<AutoForm
|
|
||||||
formSchema={formSchema}
|
|
||||||
fieldConfig={{
|
|
||||||
username: {
|
|
||||||
fieldType: "number",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("input").should("have.attr", "type", "number");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can submit valid forms", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(
|
|
||||||
<AutoForm
|
|
||||||
formSchema={formSchema}
|
|
||||||
onSubmit={(values) => {
|
|
||||||
expect(values).to.deep.equal({
|
|
||||||
username: "john",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</AutoForm>,
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("input[name=username]").type("john");
|
|
||||||
cy.get("button[type=submit]").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows error for invalid forms", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(
|
|
||||||
<AutoForm
|
|
||||||
formSchema={formSchema}
|
|
||||||
onSubmit={() => {
|
|
||||||
expect.fail("Should not be called.");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</AutoForm>,
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("button[type=submit]").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can set default values", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string().default("john"),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(<AutoForm formSchema={formSchema} />);
|
|
||||||
|
|
||||||
cy.get("input[name=username]").should("have.value", "john");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can submit with default values", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string().default("john"),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(
|
|
||||||
<AutoForm
|
|
||||||
formSchema={formSchema}
|
|
||||||
onSubmit={(values) => {
|
|
||||||
expect(values).to.deep.equal({
|
|
||||||
username: "john",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</AutoForm>,
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("button[type=submit]").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can set and submit optional values", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(
|
|
||||||
<AutoForm
|
|
||||||
formSchema={formSchema}
|
|
||||||
onSubmit={(values) => {
|
|
||||||
expect(values).to.deep.equal({
|
|
||||||
username: undefined,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</AutoForm>,
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("input[name=username]").should("have.value", "");
|
|
||||||
cy.get("button[type=submit]").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can add description", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(
|
|
||||||
<AutoForm
|
|
||||||
formSchema={formSchema}
|
|
||||||
fieldConfig={{
|
|
||||||
username: {
|
|
||||||
description: "Your username here",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("p").contains("Your username here");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can set default values on array", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
arr: z.array(z.object({ name: z.string(), age: z.number() })).default([
|
|
||||||
{ name: "Haykal", age: 21 },
|
|
||||||
{ name: "John", age: 20 },
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(<AutoForm formSchema={formSchema} />);
|
|
||||||
|
|
||||||
//get button with text Arr
|
|
||||||
cy.get("button").contains("Arr").click();
|
|
||||||
cy.get("input[name='arr.0.name']").should("have.value", "Haykal");
|
|
||||||
cy.get("input[name='arr.0.age']").should("have.value", "21");
|
|
||||||
cy.get("input[name='arr.1.name']").should("have.value", "John");
|
|
||||||
cy.get("input[name='arr.1.age']").should("have.value", "20");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can set default value of number to 0", () => {
|
|
||||||
const formSchema = z.object({
|
|
||||||
number: z.number().default(0),
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.mount(<AutoForm formSchema={formSchema} />);
|
|
||||||
|
|
||||||
cy.get("input[name='number']").should("have.value", "0");
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,76 +0,0 @@
|
|||||||
import { ControllerRenderProps, FieldValues } from "react-hook-form";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { INPUT_COMPONENTS } from "./config";
|
|
||||||
|
|
||||||
export type FieldConfigItem = {
|
|
||||||
description?: React.ReactNode;
|
|
||||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement> & {
|
|
||||||
showLabel?: boolean;
|
|
||||||
};
|
|
||||||
label?: string;
|
|
||||||
fieldType?:
|
|
||||||
| keyof typeof INPUT_COMPONENTS
|
|
||||||
| React.FC<AutoFormInputComponentProps>;
|
|
||||||
|
|
||||||
renderParent?: (props: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) => React.ReactElement | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FieldConfig<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
|
|
||||||
// If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem
|
|
||||||
[Key in keyof SchemaType]?: SchemaType[Key] extends object
|
|
||||||
? FieldConfig<z.infer<SchemaType[Key]>>
|
|
||||||
: FieldConfigItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum DependencyType {
|
|
||||||
DISABLES,
|
|
||||||
REQUIRES,
|
|
||||||
HIDES,
|
|
||||||
SETS_OPTIONS,
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
|
|
||||||
sourceField: keyof SchemaType;
|
|
||||||
type: DependencyType;
|
|
||||||
targetField: keyof SchemaType;
|
|
||||||
when: (sourceFieldValue: any, targetFieldValue: any) => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ValueDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
|
||||||
BaseDependency<SchemaType> & {
|
|
||||||
type:
|
|
||||||
| DependencyType.DISABLES
|
|
||||||
| DependencyType.REQUIRES
|
|
||||||
| DependencyType.HIDES;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EnumValues = readonly [string, ...string[]];
|
|
||||||
|
|
||||||
export type OptionsDependency<
|
|
||||||
SchemaType extends z.infer<z.ZodObject<any, any>>,
|
|
||||||
> = BaseDependency<SchemaType> & {
|
|
||||||
type: DependencyType.SETS_OPTIONS;
|
|
||||||
|
|
||||||
// Partial array of values from sourceField that will trigger the dependency
|
|
||||||
options: EnumValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Dependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
|
||||||
| ValueDependency<SchemaType>
|
|
||||||
| OptionsDependency<SchemaType>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A FormInput component can handle a specific Zod type (e.g. "ZodBoolean")
|
|
||||||
*/
|
|
||||||
export type AutoFormInputComponentProps = {
|
|
||||||
zodInputProps: React.InputHTMLAttributes<HTMLInputElement>;
|
|
||||||
field: ControllerRenderProps<FieldValues, any>;
|
|
||||||
fieldConfigItem: FieldConfigItem;
|
|
||||||
label: string;
|
|
||||||
isRequired: boolean;
|
|
||||||
fieldProps: any;
|
|
||||||
zodItem: z.ZodAny;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
@ -1,180 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { DefaultValues } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { FieldConfig } from "./types";
|
|
||||||
|
|
||||||
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
|
|
||||||
export type ZodObjectOrWrapped =
|
|
||||||
| z.ZodObject<any, any>
|
|
||||||
| z.ZodEffects<z.ZodObject<any, any>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Beautify a camelCase string.
|
|
||||||
* e.g. "myString" -> "My String"
|
|
||||||
*/
|
|
||||||
export function beautifyObjectName(string: string) {
|
|
||||||
// if numbers only return the string
|
|
||||||
let output = string.replace(/([A-Z])/g, " $1");
|
|
||||||
output = output.charAt(0).toUpperCase() + output.slice(1);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the lowest level Zod type.
|
|
||||||
* This will unpack optionals, refinements, etc.
|
|
||||||
*/
|
|
||||||
export function getBaseSchema<
|
|
||||||
ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
|
|
||||||
>(schema: ChildType | z.ZodEffects<ChildType>): ChildType | null {
|
|
||||||
if (!schema) return null;
|
|
||||||
if ("innerType" in schema._def) {
|
|
||||||
return getBaseSchema(schema._def.innerType as ChildType);
|
|
||||||
}
|
|
||||||
if ("schema" in schema._def) {
|
|
||||||
return getBaseSchema(schema._def.schema as ChildType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema as ChildType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the type name of the lowest level Zod type.
|
|
||||||
* This will unpack optionals, refinements, etc.
|
|
||||||
*/
|
|
||||||
export function getBaseType(schema: z.ZodAny): string {
|
|
||||||
const baseSchema = getBaseSchema(schema);
|
|
||||||
return baseSchema ? baseSchema._def.typeName : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for a "ZodDefult" in the Zod stack and return its value.
|
|
||||||
*/
|
|
||||||
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
|
|
||||||
const typedSchema = schema as unknown as z.ZodDefault<
|
|
||||||
z.ZodNumber | z.ZodString
|
|
||||||
>;
|
|
||||||
|
|
||||||
if (typedSchema._def.typeName === "ZodDefault") {
|
|
||||||
return typedSchema._def.defaultValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("innerType" in typedSchema._def) {
|
|
||||||
return getDefaultValueInZodStack(
|
|
||||||
typedSchema._def.innerType as unknown as z.ZodAny,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ("schema" in typedSchema._def) {
|
|
||||||
return getDefaultValueInZodStack(
|
|
||||||
(typedSchema._def as any).schema as z.ZodAny,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = DefaultValues<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;
|
|
||||||
|
|
||||||
if (getBaseType(item) === "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(
|
|
||||||
schema: ZodObjectOrWrapped,
|
|
||||||
): z.ZodObject<any, any> {
|
|
||||||
if (schema?._def.typeName === "ZodEffects") {
|
|
||||||
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>;
|
|
||||||
return getObjectFormSchema(typedSchema._def.schema);
|
|
||||||
}
|
|
||||||
return schema as z.ZodObject<any, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a Zod schema to HTML input props to give direct feedback to the user.
|
|
||||||
* Once submitted, the schema will be validated completely.
|
|
||||||
*/
|
|
||||||
export function zodToHtmlInputProps(
|
|
||||||
schema:
|
|
||||||
| z.ZodNumber
|
|
||||||
| z.ZodString
|
|
||||||
| z.ZodOptional<z.ZodNumber | z.ZodString>
|
|
||||||
| any,
|
|
||||||
): React.InputHTMLAttributes<HTMLInputElement> {
|
|
||||||
if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) {
|
|
||||||
const typedSchema = schema as z.ZodOptional<z.ZodNumber | z.ZodString>;
|
|
||||||
return {
|
|
||||||
...zodToHtmlInputProps(typedSchema._def.innerType),
|
|
||||||
required: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const typedSchema = schema as z.ZodNumber | z.ZodString;
|
|
||||||
|
|
||||||
if (!("checks" in typedSchema._def))
|
|
||||||
return {
|
|
||||||
required: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { checks } = typedSchema._def;
|
|
||||||
const inputProps: React.InputHTMLAttributes<HTMLInputElement> = {
|
|
||||||
required: true,
|
|
||||||
};
|
|
||||||
const type = getBaseType(schema);
|
|
||||||
|
|
||||||
for (const check of checks) {
|
|
||||||
if (check.kind === "min") {
|
|
||||||
if (type === "ZodString") {
|
|
||||||
inputProps.minLength = check.value;
|
|
||||||
} else {
|
|
||||||
inputProps.min = check.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (check.kind === "max") {
|
|
||||||
if (type === "ZodString") {
|
|
||||||
inputProps.maxLength = check.value;
|
|
||||||
} else {
|
|
||||||
inputProps.max = check.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputProps;
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { Calendar as CalendarIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import { forwardRef } from "react";
|
|
||||||
|
|
||||||
export const DatePicker = forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
{
|
|
||||||
date?: Date;
|
|
||||||
setDate: (date?: Date) => void;
|
|
||||||
}
|
|
||||||
>(function DatePickerCmp({ date, setDate }, ref) {
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={"outline"}
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-start text-left font-normal",
|
|
||||||
!date && "text-muted-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{date ? format(date, "PPP") : <span>Pick a date</span>}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" ref={ref}>
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={date}
|
|
||||||
onSelect={setDate}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user