- Add getFirstValidationError utility for nested FieldErrors extraction - Add mergeWithDefaults utilities (deepMergeWithDefaults, type guards) - Add useValidationErrorHandler hook for toast + tab navigation - Add FormSelect component with Controller integration - Add FormTextarea component with register integration - Refactor AgentTypeForm to use new utilities - Remove verbose debug logging (now handled by hook) - Add comprehensive tests (53 new tests, 100 total) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
134 lines
3.6 KiB
TypeScript
134 lines
3.6 KiB
TypeScript
/**
|
|
* FormSelect Component
|
|
*
|
|
* Reusable Select field with Controller integration for react-hook-form.
|
|
* Handles label, error display, and description automatically.
|
|
*
|
|
* @module components/forms/FormSelect
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { Controller, type Control, type FieldValues, type Path } from 'react-hook-form';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
|
|
export interface SelectOption {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
export interface FormSelectProps<T extends FieldValues> {
|
|
/** Field name (must be a valid path in the form) */
|
|
name: Path<T>;
|
|
/** Form control from useForm */
|
|
control: Control<T>;
|
|
/** Field label */
|
|
label: string;
|
|
/** Available options */
|
|
options: SelectOption[];
|
|
/** Is field required? Shows asterisk if true */
|
|
required?: boolean;
|
|
/** Placeholder text when no value selected */
|
|
placeholder?: string;
|
|
/** Helper text below the field */
|
|
description?: string;
|
|
/** Disable the select */
|
|
disabled?: boolean;
|
|
/** Additional class name */
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* FormSelect - Controlled Select field for react-hook-form
|
|
*
|
|
* Automatically handles:
|
|
* - Controller wrapper for react-hook-form
|
|
* - Label with required indicator
|
|
* - Error message display
|
|
* - Description/helper text
|
|
* - Accessibility attributes
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <FormSelect
|
|
* name="primary_model"
|
|
* control={form.control}
|
|
* label="Primary Model"
|
|
* required
|
|
* options={[
|
|
* { value: 'claude-opus', label: 'Claude Opus' },
|
|
* { value: 'claude-sonnet', label: 'Claude Sonnet' },
|
|
* ]}
|
|
* description="Main model used for this agent"
|
|
* />
|
|
* ```
|
|
*/
|
|
export function FormSelect<T extends FieldValues>({
|
|
name,
|
|
control,
|
|
label,
|
|
options,
|
|
required = false,
|
|
placeholder,
|
|
description,
|
|
disabled = false,
|
|
className,
|
|
}: FormSelectProps<T>) {
|
|
const selectId = String(name);
|
|
const errorId = `${selectId}-error`;
|
|
const descriptionId = description ? `${selectId}-description` : undefined;
|
|
|
|
return (
|
|
<Controller
|
|
name={name}
|
|
control={control}
|
|
render={({ field, fieldState }) => (
|
|
<div className={className}>
|
|
<div className="space-y-2">
|
|
<Label htmlFor={selectId}>
|
|
{label}
|
|
{required && <span className="text-destructive"> *</span>}
|
|
</Label>
|
|
<Select value={field.value ?? ''} onValueChange={field.onChange} disabled={disabled}>
|
|
<SelectTrigger
|
|
id={selectId}
|
|
aria-invalid={!!fieldState.error}
|
|
aria-describedby={
|
|
[fieldState.error ? errorId : null, descriptionId].filter(Boolean).join(' ') ||
|
|
undefined
|
|
}
|
|
>
|
|
<SelectValue placeholder={placeholder ?? `Select ${label.toLowerCase()}`} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{fieldState.error && (
|
|
<p id={errorId} className="text-sm text-destructive" role="alert">
|
|
{fieldState.error.message}
|
|
</p>
|
|
)}
|
|
{description && (
|
|
<p id={descriptionId} className="text-xs text-muted-foreground">
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
/>
|
|
);
|
|
}
|