330 lines
11 KiB
TypeScript
330 lines
11 KiB
TypeScript
"use client";
|
|||
|
|
|
||
|
|
import { useFormContext } from "react-hook-form";
|
||
|
|
import {
|
||
|
|
FormControl,
|
||
|
|
FormDescription,
|
||
|
|
FormField,
|
||
|
|
FormItem,
|
||
|
|
FormLabel,
|
||
|
|
FormMessage,
|
||
|
|
} from "@/components/ui/form";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
||
|
|
import { Switch } from "@/components/ui/switch";
|
||
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "@/components/ui/select";
|
||
|
|
import { Slider } from "@/components/ui/slider";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { MultiSelect } from "./controls/multi-select";
|
||
|
|
import type { ClientVariable } from "@/lib/config/serialize";
|
||
|
|
|
||
|
|
export function FieldRenderer({ variable }: { variable: ClientVariable }) {
|
||
|
|
const { control } = useFormContext();
|
||
|
|
const label = variable.label ?? variable.name;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<FormField
|
||
|
|
control={control}
|
||
|
|
name={variable.name}
|
||
|
|
render={({ field }) => {
|
||
|
|
switch (variable.control) {
|
||
|
|
case "textarea":
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>
|
||
|
|
{label}
|
||
|
|
{variable.required && (
|
||
|
|
<span className="text-destructive"> *</span>
|
||
|
|
)}
|
||
|
|
</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<Textarea {...field} value={field.value ?? ""} />
|
||
|
|
</FormControl>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "password":
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>
|
||
|
|
{label}
|
||
|
|
{variable.required && (
|
||
|
|
<span className="text-destructive"> *</span>
|
||
|
|
)}
|
||
|
|
</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<Input
|
||
|
|
type="password"
|
||
|
|
autoComplete="off"
|
||
|
|
{...field}
|
||
|
|
value={field.value ?? ""}
|
||
|
|
/>
|
||
|
|
</FormControl>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "number":
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>
|
||
|
|
{label}
|
||
|
|
{variable.required && (
|
||
|
|
<span className="text-destructive"> *</span>
|
||
|
|
)}
|
||
|
|
</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<Input
|
||
|
|
type="number"
|
||
|
|
{...(variable.type === "number"
|
||
|
|
? {
|
||
|
|
min: variable.min,
|
||
|
|
max: variable.max,
|
||
|
|
step: variable.step,
|
||
|
|
}
|
||
|
|
: {})}
|
||
|
|
value={field.value ?? ""}
|
||
|
|
onChange={(event) =>
|
||
|
|
field.onChange(
|
||
|
|
event.target.value === ""
|
||
|
|
? undefined
|
||
|
|
: Number(event.target.value),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
</FormControl>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "slider": {
|
||
|
|
const min = variable.type === "number" ? (variable.min ?? 0) : 0;
|
||
|
|
const max =
|
||
|
|
variable.type === "number" ? (variable.max ?? 100) : 100;
|
||
|
|
const step = variable.type === "number" ? variable.step : undefined;
|
||
|
|
const current =
|
||
|
|
typeof field.value === "number" ? field.value : (min + max) / 2;
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>
|
||
|
|
{label}
|
||
|
|
<span className="text-muted-foreground font-normal">
|
||
|
|
{current}
|
||
|
|
</span>
|
||
|
|
</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<Slider
|
||
|
|
min={min}
|
||
|
|
max={max}
|
||
|
|
step={step}
|
||
|
|
value={current}
|
||
|
|
onValueChange={(next) => field.onChange(next)}
|
||
|
|
/>
|
||
|
|
</FormControl>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
case "checkbox":
|
||
|
|
return (
|
||
|
|
<FormItem className="flex flex-row items-center gap-2 space-y-0">
|
||
|
|
<FormControl>
|
||
|
|
<Checkbox
|
||
|
|
checked={!!field.value}
|
||
|
|
onCheckedChange={(checked) =>
|
||
|
|
field.onChange(checked === true)
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
</FormControl>
|
||
|
|
<FormLabel className="font-normal">{label}</FormLabel>
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "switch":
|
||
|
|
return (
|
||
|
|
<FormItem className="flex flex-row items-center justify-between gap-2 space-y-0">
|
||
|
|
<FormLabel className="font-normal">{label}</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<Switch
|
||
|
|
checked={!!field.value}
|
||
|
|
onCheckedChange={(checked) => field.onChange(checked)}
|
||
|
|
/>
|
||
|
|
</FormControl>
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "select":
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>
|
||
|
|
{label}
|
||
|
|
{variable.required && (
|
||
|
|
<span className="text-destructive"> *</span>
|
||
|
|
)}
|
||
|
|
</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<Select
|
||
|
|
value={field.value ?? ""}
|
||
|
|
onValueChange={(value) => field.onChange(value)}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="w-full">
|
||
|
|
<SelectValue placeholder="Select..." />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{variable.type === "enum" &&
|
||
|
|
variable.choices.map((choice) => (
|
||
|
|
<SelectItem key={choice} value={choice}>
|
||
|
|
{choice}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</FormControl>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "radio":
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>
|
||
|
|
{label}
|
||
|
|
{variable.required && (
|
||
|
|
<span className="text-destructive"> *</span>
|
||
|
|
)}
|
||
|
|
</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<RadioGroup
|
||
|
|
value={field.value ?? ""}
|
||
|
|
onValueChange={(value) => field.onChange(value)}
|
||
|
|
>
|
||
|
|
{variable.type === "enum" &&
|
||
|
|
variable.choices.map((choice) => (
|
||
|
|
<div key={choice} className="flex items-center gap-2">
|
||
|
|
<RadioGroupItem
|
||
|
|
value={choice}
|
||
|
|
id={`${variable.name}-${choice}`}
|
||
|
|
/>
|
||
|
|
<Label
|
||
|
|
htmlFor={`${variable.name}-${choice}`}
|
||
|
|
className="font-normal"
|
||
|
|
>
|
||
|
|
{choice}
|
||
|
|
</Label>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</RadioGroup>
|
||
|
|
</FormControl>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "checkboxGroup":
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>{label}</FormLabel>
|
||
|
|
<div className="flex flex-col gap-2">
|
||
|
|
{variable.type === "multiselect" &&
|
||
|
|
variable.choices.map((choice) => {
|
||
|
|
const values: string[] = Array.isArray(field.value)
|
||
|
|
? field.value
|
||
|
|
: [];
|
||
|
|
return (
|
||
|
|
<div key={choice} className="flex items-center gap-2">
|
||
|
|
<Checkbox
|
||
|
|
checked={values.includes(choice)}
|
||
|
|
onCheckedChange={(checked) =>
|
||
|
|
field.onChange(
|
||
|
|
checked === true
|
||
|
|
? [...values, choice]
|
||
|
|
: values.filter((v) => v !== choice),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
<Label className="font-normal">{choice}</Label>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "multiselect":
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>{label}</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<MultiSelect
|
||
|
|
choices={
|
||
|
|
variable.type === "multiselect" ? variable.choices : []
|
||
|
|
}
|
||
|
|
value={Array.isArray(field.value) ? field.value : []}
|
||
|
|
onChange={(value) => field.onChange(value)}
|
||
|
|
/>
|
||
|
|
</FormControl>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "text":
|
||
|
|
default:
|
||
|
|
return (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>
|
||
|
|
{label}
|
||
|
|
{variable.required && (
|
||
|
|
<span className="text-destructive"> *</span>
|
||
|
|
)}
|
||
|
|
</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<Input {...field} value={field.value ?? ""} />
|
||
|
|
</FormControl>
|
||
|
|
{variable.description && (
|
||
|
|
<FormDescription>{variable.description}</FormDescription>
|
||
|
|
)}
|
||
|
|
<FormMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|