I assume I have made a silly error, but I keep getting a "Fetch Error" when running execute from useAction hook.
/// create-group-action.ts
"use server"
import { insertGroupSchema } from "@/server/data/schema"
import { revalidatePath } from "next/cache"
import { insertGroup } from "../queries/groups"
import { serverActionClient } from "./utils/safe-sever-actions-client"
type ActionSuccess = { success: true; status: "success" }
type ActionError = { success: false; status: "error"; errorMessage: string }
type ActionResponse = ActionSuccess | ActionError
export const createGroupAction = serverActionClient(insertGroupSchema, async (formData): Promise<ActionResponse> => {
if (!formData) return { success: false, status: "error", errorMessage: "No form data" }
await insertGroup(formData)
revalidatePath("/")
revalidatePath("/groups")
return { success: true, status: "success" }
})
export default createGroupAction
//
"use client"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { groupsTable, insertGroupSchema } from "@/server/data/schema"
import { DevTool } from "@hookform/devtools"
import { zodResolver } from "@hookform/resolvers/zod"
import { Table } from "drizzle-orm"
import { ControllerRenderProps, useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "../ui/button"
import { Checkbox } from "../ui/checkbox"
import { toast } from "sonner"
import createGroupAction from "@/server/actions/create-group-action"
import { useAction } from "next-safe-action/hooks"
import { insertGroup } from "@/server/queries/groups"
import { FormMap } from "@/app/form-hooks"
import { Input } from "../ui/input"
import { Textarea } from "../ui/textarea"
export const newGroupFormMap: FormMap = {
fields: [
{
name: "name",
label: "Group Name",
description: "This is the name of the group.",
Component: Input,
},
{
name: "description",
label: "Group Description",
description: "This is the description of the group.",
Component: Textarea,
componentProps: { placeholder: "GroupDescription" },
},
{
name: "logoUrl",
label: "Group Logo URL",
description: "This is the URL of the group logo, including the prefix (for example https://)",
Component: Input,
componentProps: { placeholder: "https://example.com/logo.png" },
},
// {
// name: "active",
// label: "Group Active",
// description: "This is whether the group is active.",
// Component: Checkbox,
// componentProps: {},
// },
],
} as const
const defaultValidationSchema = insertGroupSchema.pick({
name: true,
description: true,
active: true,
})
type NewGroupFormProps = {
tableSchema?: Table
validationSchema?: any
formMap?: typeof newGroupFormMap
}
export default function NewGroupForm({
tableSchema = groupsTable,
validationSchema = defaultValidationSchema,
formMap = newGroupFormMap,
}: NewGroupFormProps) {
const { execute, result, status } = useAction(createGroupAction, {
onSuccess: (_data, { name }) => {
toast.success(`Success: Group ${name} created`)
},
onError: (error, input) => {
if (error.fetchError) toast.error(`Fetch Error: ${error.fetchError}. Failed to create ${input.name}`)
if (error.serverError) {
console.error(`${error.serverError}`)
toast.error(`Server Error: Failed to create ${input.name}`)
}
if (error.validationErrors) toast.error(`Validation errors: ${error.validationErrors}`)
},
})
const form = useForm<z.input<typeof validationSchema>>({
resolver: zodResolver(validationSchema),
})
async function handleValid(values: z.output<typeof validationSchema>) {
execute(values)
}
const handleInvalid = () => {
// TODO: handle form submission error
toast.error("Group creation failed")
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleValid, handleInvalid)} className="flex flex-col gap-4">
{formMap.fields.map((formField) => {
const { Component, componentProps, name, label, description } = formField
const getProps = (field: ControllerRenderProps) => {
let props = { ...componentProps }
// add additional props that require `field` value for Checkbox
if (Component === Checkbox)
props = {
...componentProps,
checked: field.value,
onCheckedChange: field.onChange,
}
return { ...field, ...props }
}
return (
<FormField
key={name}
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Component {...getProps(field)} />
</FormControl>
<FormDescription>{description}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)
})}
<Button aria-disabled={status === "executing"} type="submit">
Submit
</Button>
</form>
{/* <DevTool control={form.control} /> */}
</Form>
)
}