diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index 9ffb1a1..867c2c1 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -2,7 +2,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input"; import { useMemo, useEffect } from "react"; -import { useForm } from "react-hook-form"; +import { useForm, useFieldArray } from "react-hook-form"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "@/hooks/use-toast"; import { useAppSelector } from "@/hooks/store"; @@ -10,50 +10,74 @@ import { selectHeaders } from "@/state/store"; import { ResourceSchema, PropertySchema } from "@/state/openapi"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Plus, Trash2 } from "lucide-react"; type CreateFormProps = { resource: ResourceSchema } +function createZodSchemaForType(type: string, name: string, isRequired: boolean = false): z.ZodTypeAny { + let schema: z.ZodTypeAny; + + switch (type) { + case 'integer': + schema = z.coerce.number().int({ + message: `${name} must be an integer` + }); + break; + case 'number': + schema = z.coerce.number({ + message: `${name} must be a number` + }); + break; + case 'boolean': + schema = z.coerce.boolean({ + message: `${name} must be true or false` + }); + break; + case 'object': + schema = z.record(z.any()); + break; + case 'string': + default: + if (isRequired) { + schema = z.string().min(1, { + message: `${name} is required` + }); + } else { + schema = z.string().optional(); + } + return schema; // Return early to avoid making it optional again + } + + // Make numeric, boolean, and object fields optional if not required + if (!isRequired) { + schema = schema.optional(); + } + + return schema; +} + function createValidationSchema(properties: PropertySchema[], requiredFields: string[]): z.ZodSchema { const schemaObject: Record = {}; for (const property of properties) { if (!property) continue; // Skip null properties - let fieldSchema: z.ZodTypeAny; const isRequired = requiredFields.includes(property.name); + let fieldSchema: z.ZodTypeAny; - switch (property.type) { - case 'integer': - fieldSchema = z.coerce.number().int({ - message: `${property.name} must be an integer` + if (property.type === 'array') { + const itemType = property.items?.type || 'string'; + const itemSchema = createZodSchemaForType(itemType, `${property.name} item`, true); + + fieldSchema = z.array(itemSchema); + if (isRequired) { + fieldSchema = fieldSchema.min(1, { + message: `${property.name} must have at least one item` }); - break; - case 'number': - fieldSchema = z.coerce.number({ - message: `${property.name} must be a number` - }); - break; - case 'boolean': - fieldSchema = z.coerce.boolean({ - message: `${property.name} must be true or false` - }); - break; - case 'string': - default: - if (isRequired) { - fieldSchema = z.string().min(1, { - message: `${property.name} is required` - }); - } else { - fieldSchema = z.string().optional(); - } - break; - } - - // Make numeric and boolean fields optional if not required - if (!isRequired && (property.type === 'integer' || property.type === 'number' || property.type === 'boolean')) { - fieldSchema = fieldSchema.optional(); + } + } else { + fieldSchema = createZodSchemaForType(property.type, property.name, isRequired); } schemaObject[property.name] = fieldSchema; @@ -62,6 +86,109 @@ function createValidationSchema(properties: PropertySchema[], requiredFields: st return z.object(schemaObject); } +function getInputType(propertyType: string): string { + switch (propertyType) { + case 'integer': + case 'number': + return 'number'; + case 'boolean': + return 'checkbox'; + default: + return 'text'; + } +} + +function ArrayFieldComponent({ property, control }: { property: PropertySchema, control: any }) { + const { fields, append, remove } = useFieldArray({ + control, + name: property.name, + }); + + const itemType = property.items?.type || 'string'; + const inputType = getInputType(itemType); + + const addItem = () => { + let defaultValue: any = ''; + if (itemType === 'integer' || itemType === 'number') { + defaultValue = 0; + } else if (itemType === 'boolean') { + defaultValue = false; + } else if (itemType === 'object') { + defaultValue = {}; + } + append(defaultValue); + }; + + return ( +
+ {property.name} + + {fields.map((field, index) => ( + ( + +
+ + {itemType === 'object' ? ( + { + try { + const parsed = JSON.parse(e.target.value); + inputField.onChange(parsed); + } catch { + inputField.onChange(e.target.value); + } + }} + value={typeof inputField.value === 'object' + ? JSON.stringify(inputField.value) + : inputField.value} + /> + ) : ( + inputField.onChange(e.target.checked) + : inputField.onChange + } + /> + )} + + +
+ +
+ )} + /> + ))} + + +
+ ); +} + export default function CreateForm(props: CreateFormProps) { const params = useParams(); const navigate = useNavigate(); @@ -92,48 +219,46 @@ export default function CreateForm(props: CreateFormProps) { }); const formBuilder = useMemo(() => { - return props.resource.properties().map((p) => { - if (!p) { - return (
Loading...
) - } - - const getInputType = (propertyType: string) => { - switch (propertyType) { - case 'integer': - case 'number': - return 'number'; - case 'boolean': - return 'checkbox'; - default: - return 'text'; - } - }; - + return props.resource.properties().map((p) => { + if (!p) { + return (
Loading...
) + } + + if (p.type === 'array') { return ( - ( - - {p.name} - - field.onChange(e.target.checked) - : field.onChange - } - /> - - - - )} /> - ) - }); + ); + } + + return ( + ( + + {p.name} + + field.onChange(e.target.checked) + : field.onChange + } + /> + + + + )} + /> + ) + }); }, [props, form.control]); useEffect(() => { diff --git a/src/state/openapi.ts b/src/state/openapi.ts index df181d0..f4dcd6d 100644 --- a/src/state/openapi.ts +++ b/src/state/openapi.ts @@ -73,7 +73,19 @@ class ResourceSchema { properties(): PropertySchema[] { const properties: PropertySchema[] = []; for (const [name, schema] of Object.entries(this.schema.properties)) { - properties.push(new PropertySchema(name, schema.type)); + let items: PropertySchema | undefined; + + if (schema.type === 'array' && schema.items) { + // For array items, create a PropertySchema for the items type + if (schema.items.type) { + items = new PropertySchema(`${name}_item`, schema.items.type); + } else if (schema.items.$ref) { + // Handle object references - for now, treat as 'object' + items = new PropertySchema(`${name}_item`, 'object'); + } + } + + properties.push(new PropertySchema(name, schema.type, items)); } return properties; } @@ -95,10 +107,12 @@ class ResourceSchema { class PropertySchema { name: string type: string + items?: PropertySchema - constructor(name: string, type: string) { + constructor(name: string, type: string, items?: PropertySchema) { this.name = name; this.type = type; + this.items = items; } }