I have a client component in nextjs that uses shadcn-ui,react-hook-form and zod. The client component is as follows:
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { CaretSortIcon } from '@radix-ui/react-icons';
import { useForm } from 'react-hook-form';
import { useRef } from 'react';
import { cn } from '@/app/lib/utils';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Calendar } from '@/components/ui/calendar';
import { CalendarIcon, CheckIcon } from '@radix-ui/react-icons';
import { format } from 'date-fns';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { toast } from '@/components/ui/use-toast';
import * as React from 'react';
import { VirtualizedCommand } from '@/components/ui/virtualized-combobox';
import {
RemDbStatus,
TFormUpdateSchema,
UpdateRemediation,
Option,
State,
} from '@/app/lib/definitions';
import {
fetchTechVersion,
fetchCurrentRemediationData,
UpdateRemediationAction,
} from '@/app/lib/actions';
import {
Command,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
} from '@/components/ui/command';
import { Textarea } from '@/components/ui/textarea';
// @ts-ignore
import { useActionState } from 'react';
export function ComboboxForm({
it_services,
remediation_types,
}: {
it_services: { it_service: string }[];
remediation_types: { remediation_type: string | null }[];
}) {
const form = useForm<TFormUpdateSchema>({
resolver: zodResolver(UpdateRemediation),
});
const formRef = useRef<HTMLFormElement>(null);
const { watch } = form;
console.log(`these are the form values`, form.getValues());
const options_service = it_services.map((item) => ({
value: item.it_service,
label: item.it_service,
}));
const [serviceopen, setServiceOpen] = React.useState<boolean>(false);
const [techopen, setTechOpen] = React.useState<boolean>(false);
const [remediate, setRemedDateOpen] = React.useState<boolean>(false);
const [remedtype, setRemedTypeOpen] = React.useState<boolean>(false);
const [techoptions, setTechOptions] = React.useState<Option[]>([]);
const [currentDbStatus, setcurrentDbStatus] = React.useState<RemDbStatus[]>(
[],
);
const initialState: State = { message: null, errors: {} };
const [state, formAction] = useActionState(
UpdateRemediationAction,
initialState,
);
async function onSubmit(data: TFormUpdateSchema) {
const formData = new FormData();
Object.entries(data)
.filter(([_, value]) => value !== undefined)
.forEach(([key, value]) => formData.append(key, value as string | Blob));
console.log(formData);
// await UpdateRemediationAction(formData);
}
async function showStatus(data: RemDbStatus[]) {
toast({
title: 'Current Database Status:',
description: (
<pre className="mt-2 rounded-md bg-slate-950 p-4">
<code className="text-sm text-white">
{JSON.stringify(
data.map((item) => ({
...item,
intended_remediation_date: item.intended_remediation_date,
})),
null,
2,
)}
</code>
</pre>
),
});
}
// form.handleSubmit(() => formRef.current?.submit())
return (
<Form {...form}>
<form ref={formRef} action={formAction} className="space-y-6">
<div className="rounded-md bg-gray-50 p-4 md:p-6">
<FormField
control={form.control}
name="itService"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="block text-sm font-medium">
IT Service
</FormLabel>
<Popover open={serviceopen} onOpenChange={setServiceOpen}>
<PopoverTrigger className="inline-flex self-start" asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
'justify-between text-wrap',
!field.value && 'text-muted-foreground',
)}
>
{field.value
? options_service.find(
(option) => option.value === field.value,
)?.label
: 'Select IT Service'}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<VirtualizedCommand
height="208px"
options={options_service}
placeholder="Search IT Service ..."
selectedOption={field.value}
onSelectOption={async (currentValue) => {
field.onChange(currentValue);
setServiceOpen(false);
const t_versions = await fetchTechVersion(currentValue);
setTechOptions(t_versions);
form.resetField('softwareProductVersionName');
form.resetField('intendedRemediationDate');
}}
/>
</PopoverContent>
</Popover>
<FormDescription>
This is the IT service for which the remediation date should
be updated.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="softwareProductVersionName"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="mt-6 block text-sm font-medium">
Technology Version
</FormLabel>
<Popover open={techopen} onOpenChange={setTechOpen}>
<PopoverTrigger className="inline-flex self-start" asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
'justify-between text-wrap',
!field.value && 'text-muted-foreground',
)}
disabled={!watch('itService')}
>
{field.value
? techoptions.find(
(option) =>
option.value.trim() === field.value.trim(),
)?.label
: 'Select Technology Version'}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput
placeholder="Search Technology Version"
className="h-9"
/>
<CommandList>
<CommandEmpty>
No Technology Version found.
</CommandEmpty>
<CommandGroup>
{techoptions.map((tech) => (
<CommandItem
value={tech.label}
key={tech.value}
onSelect={async (currentValue) => {
field.onChange(currentValue);
setTechOpen(false);
const {
itService,
softwareProductVersionName,
...rest
} = form.getValues();
const data = await fetchCurrentRemediationData({
software_product_version_name:
softwareProductVersionName,
it_service: itService,
});
setcurrentDbStatus(data);
await showStatus(data);
form.resetField('intendedRemediationDate');
}}
>
{tech.label}
<CheckIcon
className={cn(
'ml-auto h-4 w-4',
tech.value.trim() === field.value?.trim()
? 'opacity-100'
: 'opacity-0',
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the technology version for which the remediation date
should be updated.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="intendedRemediationDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="mt-6 block text-sm font-medium">
Remediation Date
</FormLabel>
<Popover open={remediate} onOpenChange={setRemedDateOpen}>
<PopoverTrigger
className="inline-flex gap-x-4 self-start"
asChild
>
<FormControl>
<Button
variant="outline"
className={cn(
'text-left font-normal',
!field.value && 'text-muted-foreground',
)}
disabled={
!watch('softwareProductVersionName') ||
currentDbStatus.every((obj) => obj.eim_scope)
}
>
{field.value ? (
format(field.value, 'dd/MM/yyyy')
) : (
<span>Pick a Date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date) => date < new Date('1960-01-01')}
captionLayout="dropdown-buttons"
fromYear={2000}
toYear={2050}
initialFocus
/>
</PopoverContent>
</Popover>
<FormDescription>
Pick a Remediation Date for the chosen Tech Version and IT
Service.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="remark"
render={({ field }) => {
return (
<FormItem className="flex flex-col">
<FormLabel className="mt-6">Remark</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us a little bit about your remediation journey"
className="w-1/2 resize-none"
{...field}
disabled={watch([
'itService',
'softwareProductVersionName',
]).every((i) => i === undefined)}
/>
</FormControl>
<FormDescription>
Comments you may like to store about remediation
information.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="remediationType"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="mt-6 block text-sm font-medium">
Remediation Type
</FormLabel>
<Popover open={remedtype} onOpenChange={setRemedTypeOpen}>
<PopoverTrigger className="inline-flex self-start" asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
'justify-between text-wrap',
!field.value && 'text-muted-foreground',
)}
disabled={watch([
'itService',
'softwareProductVersionName',
]).every((i) => i === undefined)}
>
{field.value
? remediation_types.find(
(option) =>
option.remediation_type === field.value,
)?.remediation_type
: 'Select Remediation Type'}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="inline-flex self-start p-0">
<Command>
<CommandInput
placeholder="Search Remediation Type"
className="h-9"
/>
<CommandList>
<CommandEmpty>No Remediation type found.</CommandEmpty>
<CommandGroup>
{remediation_types.map((remed) => (
<CommandItem
value={remed.remediation_type!}
key={remed.remediation_type}
onSelect={async (currentValue) => {
field.onChange(currentValue);
// form.setValue('remediationType', currentValue);
setRemedTypeOpen(false);
}}
>
{remed.remediation_type}
<CheckIcon
className={cn(
'ml-auto h-4 w-4',
remed.remediation_type!.trim() === field.value
? 'opacity-100'
: 'opacity-0',
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
Remediation Types of a Product
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="mt-6">
Submit
</Button>
</div>
</form>
</Form>
);
}
I then use a server action to take in formdata from react-hook-form and update my database as in this file:
'use server';
import { evergreen, remediation } from '@/drizzle/schema';
import { db } from '@/drizzle/db';
import { sql, eq, and } from 'drizzle-orm';
import { UpdateRemediation } from './definitions';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { State } from './definitions';
export async function UpdateRemediationAction(
prevState: State,
formdata: FormData,
) {
const rawFormData = Object.fromEntries(formdata) as Record<string, string>;
console.log(`this is raw form`, rawFormData);
const parsedData = UpdateRemediation.safeParse(rawFormData);
// console.log(parsedData.error);
if (!parsedData.success) {
return {
errors: parsedData.error.flatten().fieldErrors,
message: 'Invalid form data',
};
}
const { itService, softwareProductVersionName, ...rest } = parsedData.data;
const objtoUpdate = {
...rest,
intendedRemediationDate:
parsedData.data.intendedRemediationDate?.toISOString(),
};
const updateData = Object.fromEntries(
Object.entries([objtoUpdate]).filter(([_, value]) => value !== undefined),
);
try {
await db
.update(evergreen)
.set(updateData)
.where(
and(
eq(evergreen.itService, itService),
eq(evergreen.softwareProductVersionName, softwareProductVersionName),
),
);
revalidatePath('/dashboard/inventory');
} catch (error) {
// throw new Error(`Database Error: ${error}`);
// console.log(error);
// return {
// message: `Database Error: ${error}`,
// };
}
redirect('/dashboard/inventory');
// if (Object.keys(updateData).length === 0) {
// console.log({
// message: 'Form Data Cannot Be Empty',
// });
// return {
// message: 'Form Data Cannot Be Empty',
// };
// } else {
// revalidatePath('/dashboard/inventory');
// redirect('/dashboard/inventory');
// }
}
And here is my zod schema:
const FormSchema = z.object({
id: z.string(),
itService: z.string({
required_error: 'Please select an IT Service.',
}),
softwareProductVersionName: z.string({
required_error: 'Please select a technology version',
}),
intendedRemediationDate: z.date().optional(),
remark: z.string().optional(),
// .transform((val) => (val === '' ? undefined : val))
// .refine(
// (val) =>
// val === undefined ||
// val === '' ||
// (val.length >= 10 && val.length <= 100),
// {
// message:
// 'Remark must be at least 10 characters and at most 100 characters, or left blank.',
// },)
remediationType: z.string().optional(),
modifyAt: z.string(),
});
export const UpdateRemediation = FormSchema.omit({ id: true, modifyAt: true });
export type TFormSchema = z.infer<typeof FormSchema>;
export type TFormUpdateSchema = z.infer<typeof UpdateRemediation>;
export type State = {
errors?: {
intendedRemediationDate?: string[];
remark?: string[];
remediationType?: string[];
};
message?: string | null;
};
I have a problem that for some unknown reason, only the field remark
seems to be passed to the server action, here is the outout of console.log
{remark:’llllllllllllllllllllllllllllllllllllllllllllllllllllllllll’}