Add a searchable combobox control for enum variables
select/radio don't scale to enums with dozens of choices. Reuses the same cmdk Command/Popover primitives multi-select already uses, just single-valued instead of an array.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ComboboxProps {
|
||||
choices: string[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function Combobox({
|
||||
choices,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Select...",
|
||||
}: ComboboxProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-auto min-h-8 w-full justify-between font-normal",
|
||||
)}
|
||||
>
|
||||
<span className={cn("flex-1 text-left", !value && "text-muted-foreground")}>
|
||||
{value || placeholder}
|
||||
</span>
|
||||
<ChevronsUpDown className="text-muted-foreground size-4 shrink-0" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No matches.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{choices.map((choice) => (
|
||||
<CommandItem
|
||||
key={choice}
|
||||
onSelect={() => {
|
||||
onChange(choice);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 size-4",
|
||||
value === choice ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{choice}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { MultiSelect } from "./controls/multi-select";
|
||||
import { Combobox } from "./controls/combobox";
|
||||
import type { ClientVariable } from "@/lib/config/serialize";
|
||||
|
||||
export function FieldRenderer({ variable }: { variable: ClientVariable }) {
|
||||
@@ -211,6 +212,31 @@ export function FieldRenderer({ variable }: { variable: ClientVariable }) {
|
||||
</FormItem>
|
||||
);
|
||||
|
||||
case "combobox":
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{label}
|
||||
{variable.required && (
|
||||
<span className="text-destructive"> *</span>
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
choices={
|
||||
variable.type === "enum" ? variable.choices : []
|
||||
}
|
||||
value={field.value ?? ""}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
/>
|
||||
</FormControl>
|
||||
{variable.description && (
|
||||
<FormDescription>{variable.description}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
|
||||
case "radio":
|
||||
return (
|
||||
<FormItem>
|
||||
|
||||
@@ -18,6 +18,7 @@ const controlSchema = z.enum([
|
||||
"switch",
|
||||
"select",
|
||||
"radio",
|
||||
"combobox",
|
||||
"multiselect",
|
||||
"checkboxGroup",
|
||||
]);
|
||||
@@ -115,6 +116,13 @@ const variableWithChecks = variableSchema.superRefine((variable, ctx) => {
|
||||
path: ["control"],
|
||||
});
|
||||
}
|
||||
if (variable.control === "combobox" && variable.type !== "enum") {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "control 'combobox' requires type 'enum'",
|
||||
path: ["control"],
|
||||
});
|
||||
}
|
||||
if (
|
||||
variable.control === "slider" &&
|
||||
variable.type === "number" &&
|
||||
|
||||
Reference in New Issue
Block a user