forked from cardosofelipe/fast-next-template
- 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>
119 lines
3.0 KiB
TypeScript
119 lines
3.0 KiB
TypeScript
/**
|
|
* Validation Error Handler Hook
|
|
*
|
|
* Handles client-side Zod/react-hook-form validation errors with:
|
|
* - Toast notifications
|
|
* - Optional tab navigation
|
|
* - Debug logging
|
|
*
|
|
* @module lib/forms/hooks/useValidationErrorHandler
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useCallback } from 'react';
|
|
import { toast } from 'sonner';
|
|
import type { FieldErrors, FieldValues } from 'react-hook-form';
|
|
import { getFirstValidationError } from '../utils/getFirstValidationError';
|
|
|
|
export interface TabFieldMapping {
|
|
/** Map of field names to tab values */
|
|
[fieldName: string]: string;
|
|
}
|
|
|
|
export interface UseValidationErrorHandlerOptions {
|
|
/**
|
|
* Map of field names (top-level) to tab values.
|
|
* When an error occurs, navigates to the tab containing the field.
|
|
*/
|
|
tabMapping?: TabFieldMapping;
|
|
|
|
/**
|
|
* Callback to set the active tab.
|
|
* Required if tabMapping is provided.
|
|
*/
|
|
setActiveTab?: (tab: string) => void;
|
|
|
|
/**
|
|
* Enable debug logging to console.
|
|
* @default false in production, true in development
|
|
*/
|
|
debug?: boolean;
|
|
|
|
/**
|
|
* Toast title for validation errors.
|
|
* @default 'Please fix form errors'
|
|
*/
|
|
toastTitle?: string;
|
|
}
|
|
|
|
export interface UseValidationErrorHandlerReturn<T extends FieldValues> {
|
|
/**
|
|
* Handler function to pass to react-hook-form's handleSubmit second argument.
|
|
* Shows toast, navigates to tab, and logs errors.
|
|
*/
|
|
onValidationError: (errors: FieldErrors<T>) => void;
|
|
}
|
|
|
|
/**
|
|
* Hook for handling client-side validation errors
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const [activeTab, setActiveTab] = useState('basic');
|
|
*
|
|
* const { onValidationError } = useValidationErrorHandler({
|
|
* tabMapping: {
|
|
* name: 'basic',
|
|
* slug: 'basic',
|
|
* primary_model: 'model',
|
|
* model_params: 'model',
|
|
* },
|
|
* setActiveTab,
|
|
* });
|
|
*
|
|
* // In form:
|
|
* <form onSubmit={handleSubmit(onSuccess, onValidationError)}>
|
|
* ```
|
|
*/
|
|
export function useValidationErrorHandler<T extends FieldValues>(
|
|
options: UseValidationErrorHandlerOptions = {}
|
|
): UseValidationErrorHandlerReturn<T> {
|
|
const {
|
|
tabMapping,
|
|
setActiveTab,
|
|
debug = process.env.NODE_ENV === 'development',
|
|
toastTitle = 'Please fix form errors',
|
|
} = options;
|
|
|
|
const onValidationError = useCallback(
|
|
(errors: FieldErrors<T>) => {
|
|
// Log errors in debug mode
|
|
if (debug) {
|
|
console.error('[Form Validation] Errors:', errors);
|
|
}
|
|
|
|
// Get first error for toast
|
|
const firstError = getFirstValidationError(errors);
|
|
if (!firstError) return;
|
|
|
|
// Show toast
|
|
toast.error(toastTitle, {
|
|
description: `${firstError.field}: ${firstError.message}`,
|
|
});
|
|
|
|
// Navigate to tab if mapping provided
|
|
if (tabMapping && setActiveTab) {
|
|
const topLevelField = firstError.field.split('.')[0];
|
|
const targetTab = tabMapping[topLevelField];
|
|
if (targetTab) {
|
|
setActiveTab(targetTab);
|
|
}
|
|
}
|
|
},
|
|
[tabMapping, setActiveTab, debug, toastTitle]
|
|
);
|
|
|
|
return { onValidationError };
|
|
}
|