"use client" import { useState } from "react" import { useFormContext, useController, type FieldValues, type FieldPath, } from "react-hook-form" import { useQuery, useQueryClient } from "@tanstack/react-query" import { Check, PlusIcon, X } from "lucide-react" import { useAuthApi } from "@/shared/useApi" import { LABEL_ROUTES } from "@garage/api" import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover" import { Button } from "@/shared/components/ui/button" import { Input } from "@/shared/components/ui/input" import { ScrollArea } from "@/shared/components/ui/scroll-area" import { Field, FieldLabel, FieldError, FieldDescription, } from "@/shared/components/ui/field" import { cn } from "@/shared/lib/utils" // ── Types ── export type LabelItem = { id: number title: string color_code: string } type RhfLabelPickerFieldProps< TValues extends FieldValues, TName extends FieldPath, > = { name: TName label?: string description?: string required?: boolean disabled?: boolean placeholder?: string } // ── Helpers ── function extractLabels(response: unknown): LabelItem[] { if (Array.isArray(response)) return response const obj = response as any if (Array.isArray(obj?.data?.data)) return obj.data.data if (Array.isArray(obj?.data)) return obj.data return [] } // ── Component ── export function RhfLabelPickerField< TValues extends FieldValues, TName extends FieldPath, >({ name, label, description, required, disabled, placeholder = "Select labels...", }: RhfLabelPickerFieldProps) { const api = useAuthApi() const queryClient = useQueryClient() const { control } = useFormContext() const { field, fieldState: { error } } = useController({ name, control, disabled }) const [open, setOpen] = useState(false) const [search, setSearch] = useState("") const [creating, setCreating] = useState(false) const [newTitle, setNewTitle] = useState("") const [newColor, setNewColor] = useState("#6366f1") const [isSubmitting, setIsSubmitting] = useState(false) const { data: allLabels = [] } = useQuery({ queryKey: [LABEL_ROUTES.INDEX], queryFn: async () => { const res = await api.labels.list() return extractLabels(res) }, staleTime: 5 * 60 * 1000, }) const selected: LabelItem[] = field.value ?? [] const filtered = search ? allLabels.filter((l) => l.title.toLowerCase().includes(search.toLowerCase()), ) : allLabels function toggle(lbl: LabelItem) { const isSelected = selected.some((s) => s.id === lbl.id) if (isSelected) { field.onChange(selected.filter((s) => s.id !== lbl.id)) } else { field.onChange([...selected, lbl]) } } function remove(id: number, e: React.MouseEvent) { e.stopPropagation() field.onChange(selected.filter((s) => s.id !== id)) } async function handleCreate() { if (!newTitle.trim()) return setIsSubmitting(true) try { const res = await api.labels.create({ title: newTitle.trim(), color_code: newColor, }) as any const created = res?.data ?? res queryClient.invalidateQueries({ queryKey: [LABEL_ROUTES.INDEX] }) if (created?.id) { field.onChange([ ...selected, { id: created.id, title: created.title ?? newTitle.trim(), color_code: created.color_code ?? newColor, }, ]) } setNewTitle("") setNewColor("#6366f1") setCreating(false) } catch { // silent – toast handled upstream if desired } finally { setIsSubmitting(false) } } return ( {label && ( {label} {required && ( * )} )}
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault() setOpen((v) => !v) } }} className={cn( "flex min-h-9 w-full cursor-pointer flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-3 py-1.5 text-sm transition-colors hover:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", disabled && "cursor-not-allowed opacity-50", error && "border-destructive", )} > {selected.length === 0 && ( {placeholder} )} {selected.map((s) => ( {s.title} ))}
{/* Search */}
setSearch(e.target.value)} className="h-8" />
{/* List */}
{filtered.length === 0 && (

No labels found

)} {filtered.map((lbl) => { const isSelected = selected.some( (s) => s.id === lbl.id, ) return ( ) })}
{/* Footer: create */}
{creating ? (
setNewTitle(e.target.value)} className="h-8" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() handleCreate() } if (e.key === "Escape") setCreating(false) }} autoFocus />
setNewColor(e.target.value) } className="h-8 w-8 cursor-pointer rounded border p-0.5" title="Pick a color" /> Color
) : ( )}
{description && ( {description} )} {error && {error.message}}
) }