@djangocfg/layouts 1.2.32 → 1.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ /**
2
+ * ValidationErrorContext - Global Zod validation error tracking
3
+ *
4
+ * Automatically captures all Zod validation errors from API client
5
+ * using browser CustomEvent API and provides centralized state management.
6
+ *
7
+ * Features:
8
+ * - Automatic error capture via window.addEventListener
9
+ * - Toast notifications for user feedback
10
+ * - Error history with timestamps
11
+ * - Configurable error limits
12
+ * - React Context API for component access
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * import { useValidationErrors } from '@djangocfg/layouts';
17
+ *
18
+ * function MyComponent() {
19
+ * const { errors, clearErrors, clearError } = useValidationErrors();
20
+ *
21
+ * return (
22
+ * <div>
23
+ * <h2>Recent Validation Errors ({errors.length})</h2>
24
+ * {errors.map((error) => (
25
+ * <div key={error.id}>
26
+ * <strong>{error.operation}</strong>
27
+ * <p>{error.timestamp.toLocaleString()}</p>
28
+ * <button onClick={() => clearError(error.id)}>Clear</button>
29
+ * </div>
30
+ * ))}
31
+ * </div>
32
+ * );
33
+ * }
34
+ * ```
35
+ */
36
+
37
+ 'use client';
38
+
39
+ import React, { createContext, useContext, useCallback, useEffect, useState, ReactNode } from 'react';
40
+ import type { ZodError } from 'zod';
41
+ import { toast } from '@djangocfg/ui';
42
+ import { createValidationErrorToast } from './ValidationErrorToast';
43
+
44
+ /**
45
+ * Validation error detail from CustomEvent
46
+ */
47
+ export interface ValidationErrorDetail {
48
+ /** Operation/function name that failed validation */
49
+ operation: string;
50
+ /** API endpoint path */
51
+ path: string;
52
+ /** HTTP method */
53
+ method: string;
54
+ /** Zod validation error */
55
+ error: ZodError;
56
+ /** Raw response data that failed validation */
57
+ response: any;
58
+ /** Timestamp of the error */
59
+ timestamp: Date;
60
+ }
61
+
62
+ /**
63
+ * Stored validation error with unique ID
64
+ */
65
+ export interface StoredValidationError extends ValidationErrorDetail {
66
+ /** Unique identifier for this error instance */
67
+ id: string;
68
+ }
69
+
70
+ /**
71
+ * Configuration for ValidationErrorProvider
72
+ */
73
+ export interface ValidationErrorConfig {
74
+ /**
75
+ * Maximum number of errors to keep in history
76
+ * @default 50
77
+ */
78
+ maxErrors?: number;
79
+
80
+ /**
81
+ * Enable toast notifications on validation errors
82
+ * @default true
83
+ */
84
+ enableToast?: boolean;
85
+
86
+ /**
87
+ * Toast notification settings
88
+ */
89
+ toastSettings?: {
90
+ /** Show operation name in toast */
91
+ showOperation?: boolean;
92
+ /** Show endpoint path in toast */
93
+ showPath?: boolean;
94
+ /** Show error count in toast */
95
+ showErrorCount?: boolean;
96
+ /** Toast duration in milliseconds (0 = no auto-dismiss) */
97
+ duration?: number;
98
+ };
99
+
100
+ /**
101
+ * Custom error handler (called before toast)
102
+ * Return false to prevent default toast notification
103
+ */
104
+ onError?: (error: ValidationErrorDetail) => boolean | void;
105
+ }
106
+
107
+ /**
108
+ * Context value
109
+ */
110
+ export interface ValidationErrorContextValue {
111
+ /** Array of validation errors */
112
+ errors: StoredValidationError[];
113
+
114
+ /** Clear all errors */
115
+ clearErrors: () => void;
116
+
117
+ /** Clear specific error by ID */
118
+ clearError: (id: string) => void;
119
+
120
+ /** Get error count */
121
+ errorCount: number;
122
+
123
+ /** Get latest error */
124
+ latestError: StoredValidationError | null;
125
+
126
+ /** Configuration */
127
+ config: Required<ValidationErrorConfig>;
128
+ }
129
+
130
+ const ValidationErrorContext = createContext<ValidationErrorContextValue | undefined>(undefined);
131
+
132
+ /**
133
+ * Default configuration
134
+ */
135
+ const defaultConfig: Required<ValidationErrorConfig> = {
136
+ maxErrors: 50,
137
+ enableToast: true,
138
+ toastSettings: {
139
+ showOperation: true,
140
+ showPath: true,
141
+ showErrorCount: true,
142
+ duration: 8000, // 8 seconds
143
+ },
144
+ onError: () => true,
145
+ };
146
+
147
+ /**
148
+ * Generate unique ID for error
149
+ */
150
+ let errorIdCounter = 0;
151
+ function generateErrorId(): string {
152
+ return `validation-error-${Date.now()}-${++errorIdCounter}`;
153
+ }
154
+
155
+ /**
156
+ * Format Zod error issues for display
157
+ */
158
+ function formatZodIssues(error: ZodError): string {
159
+ const issues = error.issues.slice(0, 3); // Show max 3 issues
160
+ const formatted = issues.map((issue) => {
161
+ const path = issue.path.join('.') || 'root';
162
+ return `${path}: ${issue.message}`;
163
+ });
164
+
165
+ if (error.issues.length > 3) {
166
+ formatted.push(`... and ${error.issues.length - 3} more`);
167
+ }
168
+
169
+ return formatted.join(', ');
170
+ }
171
+
172
+ export interface ValidationErrorProviderProps {
173
+ children: ReactNode;
174
+ config?: Partial<ValidationErrorConfig>;
175
+ }
176
+
177
+ /**
178
+ * ValidationErrorProvider Component
179
+ *
180
+ * Wraps application and provides validation error tracking.
181
+ * Automatically listens to 'zod-validation-error' CustomEvents
182
+ * from the API client.
183
+ */
184
+ export function ValidationErrorProvider({ children, config: userConfig }: ValidationErrorProviderProps) {
185
+ const [errors, setErrors] = useState<StoredValidationError[]>([]);
186
+
187
+ // Merge user config with defaults
188
+ const config: Required<ValidationErrorConfig> = {
189
+ ...defaultConfig,
190
+ ...userConfig,
191
+ toastSettings: {
192
+ ...defaultConfig.toastSettings,
193
+ ...userConfig?.toastSettings,
194
+ },
195
+ };
196
+
197
+ /**
198
+ * Clear all errors
199
+ */
200
+ const clearErrors = useCallback(() => {
201
+ setErrors([]);
202
+ }, []);
203
+
204
+ /**
205
+ * Clear specific error
206
+ */
207
+ const clearError = useCallback((id: string) => {
208
+ setErrors((prev) => prev.filter((error) => error.id !== id));
209
+ }, []);
210
+
211
+ /**
212
+ * Handle validation error event
213
+ */
214
+ const handleValidationError = useCallback(
215
+ (event: Event) => {
216
+ if (!(event instanceof CustomEvent)) return;
217
+
218
+ const detail = event.detail as ValidationErrorDetail;
219
+
220
+ // Create stored error with ID
221
+ const storedError: StoredValidationError = {
222
+ ...detail,
223
+ id: generateErrorId(),
224
+ };
225
+
226
+ // Add to errors array (limited by maxErrors)
227
+ setErrors((prev) => {
228
+ const updated = [storedError, ...prev];
229
+ return updated.slice(0, config.maxErrors);
230
+ });
231
+
232
+ // Call custom error handler
233
+ const shouldShowToast = config.onError?.(detail) !== false;
234
+
235
+ // Show toast notification with copy button
236
+ if (config.enableToast && shouldShowToast) {
237
+ const { showOperation, showPath, showErrorCount } = config.toastSettings;
238
+
239
+ // Create toast with custom copy action button
240
+ const toastOptions = createValidationErrorToast(detail, {
241
+ config: {
242
+ showOperation,
243
+ showPath,
244
+ showErrorCount,
245
+ maxIssuesInDescription: 3,
246
+ titlePrefix: '❌ Validation Error',
247
+ },
248
+ duration: config.toastSettings.duration,
249
+ onCopySuccess: () => {
250
+ // Show success feedback when error details are copied
251
+ toast({
252
+ title: '✅ Copied!',
253
+ description: 'Error details copied to clipboard',
254
+ duration: 2000,
255
+ });
256
+ },
257
+ onCopyError: (error) => {
258
+ // Show error feedback if copy fails
259
+ toast({
260
+ title: '❌ Copy failed',
261
+ description: 'Could not copy error details',
262
+ variant: 'destructive',
263
+ duration: 2000,
264
+ });
265
+ },
266
+ });
267
+
268
+ toast(toastOptions);
269
+ }
270
+ },
271
+ [config]
272
+ );
273
+
274
+ /**
275
+ * Setup event listener
276
+ */
277
+ useEffect(() => {
278
+ // Only run in browser
279
+ if (typeof window === 'undefined') return;
280
+
281
+ window.addEventListener('zod-validation-error', handleValidationError);
282
+
283
+ return () => {
284
+ window.removeEventListener('zod-validation-error', handleValidationError);
285
+ };
286
+ }, [handleValidationError]);
287
+
288
+ const value: ValidationErrorContextValue = {
289
+ errors,
290
+ clearErrors,
291
+ clearError,
292
+ errorCount: errors.length,
293
+ latestError: errors[0] || null,
294
+ config,
295
+ };
296
+
297
+ return (
298
+ <ValidationErrorContext.Provider value={value}>
299
+ {children}
300
+ </ValidationErrorContext.Provider>
301
+ );
302
+ }
303
+
304
+ /**
305
+ * useValidationErrors Hook
306
+ *
307
+ * Access validation errors from any component
308
+ *
309
+ * @example
310
+ * ```tsx
311
+ * function ErrorPanel() {
312
+ * const { errors, clearErrors } = useValidationErrors();
313
+ *
314
+ * if (errors.length === 0) return null;
315
+ *
316
+ * return (
317
+ * <div>
318
+ * <h3>Validation Errors ({errors.length})</h3>
319
+ * <button onClick={clearErrors}>Clear All</button>
320
+ * </div>
321
+ * );
322
+ * }
323
+ * ```
324
+ */
325
+ export function useValidationErrors(): ValidationErrorContextValue {
326
+ const context = useContext(ValidationErrorContext);
327
+
328
+ if (context === undefined) {
329
+ throw new Error('useValidationErrors must be used within ValidationErrorProvider');
330
+ }
331
+
332
+ return context;
333
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * ValidationErrorToast Component
3
+ *
4
+ * Custom toast for validation errors with copy button
5
+ * Formats Zod validation errors and provides one-click copy functionality
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+ import { ToastAction } from '@djangocfg/ui';
12
+ import type { ZodError } from 'zod';
13
+
14
+ export interface ValidationErrorDetail {
15
+ operation: string;
16
+ path: string;
17
+ method: string;
18
+ error: ZodError;
19
+ response: any;
20
+ timestamp: Date;
21
+ }
22
+
23
+ export interface ValidationErrorToastConfig {
24
+ /** Show operation name in title */
25
+ showOperation?: boolean;
26
+ /** Show API path in description */
27
+ showPath?: boolean;
28
+ /** Show error count in description */
29
+ showErrorCount?: boolean;
30
+ /** Maximum number of issues to show in description */
31
+ maxIssuesInDescription?: number;
32
+ /** Custom title prefix */
33
+ titlePrefix?: string;
34
+ }
35
+
36
+ const defaultConfig: ValidationErrorToastConfig = {
37
+ showOperation: true,
38
+ showPath: true,
39
+ showErrorCount: true,
40
+ maxIssuesInDescription: 3,
41
+ titlePrefix: '❌ Validation Error',
42
+ };
43
+
44
+ /**
45
+ * Format Zod error issues for display in toast description
46
+ */
47
+ export function formatZodIssuesForToast(
48
+ error: ZodError,
49
+ maxIssues: number = 3
50
+ ): string {
51
+ const issues = error.issues.slice(0, maxIssues);
52
+ const formatted = issues.map((issue) => {
53
+ const path = issue.path.join('.') || 'root';
54
+ return `${path}: ${issue.message}`;
55
+ });
56
+
57
+ if (error.issues.length > maxIssues) {
58
+ formatted.push(`... and ${error.issues.length - maxIssues} more`);
59
+ }
60
+
61
+ return formatted.join(', ');
62
+ }
63
+
64
+ /**
65
+ * Format full error details for copying to clipboard
66
+ * Includes all error information in structured JSON format
67
+ */
68
+ export function formatErrorForClipboard(detail: ValidationErrorDetail): string {
69
+ const errorData = {
70
+ timestamp: detail.timestamp.toISOString(),
71
+ operation: detail.operation,
72
+ endpoint: {
73
+ method: detail.method,
74
+ path: detail.path,
75
+ },
76
+ validation_errors: detail.error.issues.map((issue) => ({
77
+ path: issue.path.join('.') || 'root',
78
+ message: issue.message,
79
+ code: issue.code,
80
+ ...(('expected' in issue) && { expected: issue.expected }),
81
+ ...(('received' in issue) && { received: issue.received }),
82
+ ...(('minimum' in issue) && { minimum: issue.minimum }),
83
+ ...(('maximum' in issue) && { maximum: issue.maximum }),
84
+ })),
85
+ response: detail.response,
86
+ total_errors: detail.error.issues.length,
87
+ };
88
+
89
+ return JSON.stringify(errorData, null, 2);
90
+ }
91
+
92
+ /**
93
+ * Copy text to clipboard
94
+ */
95
+ async function copyToClipboard(text: string): Promise<boolean> {
96
+ if (typeof window === 'undefined') return false;
97
+
98
+ try {
99
+ if (navigator.clipboard && navigator.clipboard.writeText) {
100
+ await navigator.clipboard.writeText(text);
101
+ return true;
102
+ } else {
103
+ // Fallback for older browsers
104
+ const textarea = document.createElement('textarea');
105
+ textarea.value = text;
106
+ textarea.style.position = 'fixed';
107
+ textarea.style.opacity = '0';
108
+ document.body.appendChild(textarea);
109
+ textarea.select();
110
+ const success = document.execCommand('copy');
111
+ document.body.removeChild(textarea);
112
+ return success;
113
+ }
114
+ } catch (error) {
115
+ console.error('Failed to copy to clipboard:', error);
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Build toast title from validation error
122
+ */
123
+ export function buildToastTitle(
124
+ detail: ValidationErrorDetail,
125
+ config: ValidationErrorToastConfig = defaultConfig
126
+ ): string {
127
+ const titleParts: string[] = [config.titlePrefix || '❌ Validation Error'];
128
+
129
+ if (config.showOperation) {
130
+ titleParts.push(`in ${detail.operation}`);
131
+ }
132
+
133
+ return titleParts.join(' ');
134
+ }
135
+
136
+ /**
137
+ * Build toast description from validation error
138
+ */
139
+ export function buildToastDescription(
140
+ detail: ValidationErrorDetail,
141
+ config: ValidationErrorToastConfig = defaultConfig
142
+ ): string {
143
+ const descriptionParts: string[] = [];
144
+
145
+ // Add HTTP method and path
146
+ if (config.showPath) {
147
+ descriptionParts.push(`${detail.method} ${detail.path}`);
148
+ }
149
+
150
+ // Add error count
151
+ if (config.showErrorCount) {
152
+ const count = detail.error.issues.length;
153
+ const plural = count === 1 ? 'error' : 'errors';
154
+ descriptionParts.push(`${count} ${plural}`);
155
+ }
156
+
157
+ // Add formatted error messages
158
+ descriptionParts.push(
159
+ formatZodIssuesForToast(
160
+ detail.error,
161
+ config.maxIssuesInDescription
162
+ )
163
+ );
164
+
165
+ return descriptionParts.join(' • ');
166
+ }
167
+
168
+ /**
169
+ * Create copy action button for toast
170
+ *
171
+ * @param detail - Validation error details
172
+ * @param onCopySuccess - Optional callback when copy succeeds
173
+ * @param onCopyError - Optional callback when copy fails
174
+ */
175
+ export function createCopyAction(
176
+ detail: ValidationErrorDetail,
177
+ onCopySuccess?: () => void,
178
+ onCopyError?: (error: Error) => void
179
+ ): React.ReactElement<typeof ToastAction> {
180
+ const handleCopy = async (e: React.MouseEvent) => {
181
+ e.preventDefault();
182
+ e.stopPropagation();
183
+
184
+ try {
185
+ const formattedError = formatErrorForClipboard(detail);
186
+ const success = await copyToClipboard(formattedError);
187
+
188
+ if (success) {
189
+ console.log('✅ Validation error copied to clipboard');
190
+ onCopySuccess?.();
191
+ } else {
192
+ throw new Error('Clipboard API failed');
193
+ }
194
+ } catch (error) {
195
+ console.error('❌ Failed to copy validation error:', error);
196
+ onCopyError?.(error as Error);
197
+ }
198
+ };
199
+
200
+ return (
201
+ <ToastAction
202
+ altText="Copy error details"
203
+ onClick={handleCopy}
204
+ className="shrink-0"
205
+ >
206
+ 📋 Copy
207
+ </ToastAction>
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Create complete toast options for validation error
213
+ *
214
+ * Usage:
215
+ * ```typescript
216
+ * const { toast } = useToast();
217
+ *
218
+ * const toastOptions = createValidationErrorToast(errorDetail, {
219
+ * onCopySuccess: () => toast({ title: '✅ Copied!' }),
220
+ * onCopyError: () => toast({ title: '❌ Copy failed', variant: 'destructive' }),
221
+ * });
222
+ *
223
+ * toast(toastOptions);
224
+ * ```
225
+ */
226
+ export function createValidationErrorToast(
227
+ detail: ValidationErrorDetail,
228
+ options?: {
229
+ config?: Partial<ValidationErrorToastConfig>;
230
+ duration?: number;
231
+ onCopySuccess?: () => void;
232
+ onCopyError?: (error: Error) => void;
233
+ }
234
+ ) {
235
+ const config: ValidationErrorToastConfig = {
236
+ ...defaultConfig,
237
+ ...options?.config,
238
+ };
239
+
240
+ return {
241
+ title: buildToastTitle(detail, config),
242
+ description: buildToastDescription(detail, config),
243
+ variant: 'destructive' as const,
244
+ duration: options?.duration,
245
+ action: createCopyAction(
246
+ detail,
247
+ options?.onCopySuccess,
248
+ options?.onCopyError
249
+ ),
250
+ };
251
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Validation Error Tracking
3
+ *
4
+ * Automatic capture and management of Zod validation errors
5
+ * from API client using browser CustomEvent API.
6
+ */
7
+
8
+ export {
9
+ ValidationErrorProvider,
10
+ useValidationErrors,
11
+ type ValidationErrorDetail,
12
+ type StoredValidationError,
13
+ type ValidationErrorConfig,
14
+ type ValidationErrorContextValue,
15
+ } from './ValidationErrorContext';
16
+
17
+ export {
18
+ createValidationErrorToast,
19
+ createCopyAction,
20
+ buildToastTitle,
21
+ buildToastDescription,
22
+ formatZodIssuesForToast,
23
+ formatErrorForClipboard,
24
+ type ValidationErrorToastConfig,
25
+ } from './ValidationErrorToast';