@djangocfg/layouts 1.2.32 → 1.2.34

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,162 @@
1
+ # Validation Error System - Refactoring Summary
2
+
3
+ ## Changes Made
4
+
5
+ ### ✅ Added: Copy as cURL Feature
6
+
7
+ **New Files:**
8
+ - `curl-generator.ts` - cURL command generation
9
+ - `ValidationErrorButtons.tsx` - Button component using `useCopy` hook
10
+
11
+ **Key Improvements:**
12
+ 1. **Auto token injection** - Token automatically fetched from localStorage
13
+ 2. **useCopy integration** - Uses existing `useCopy` hook instead of custom clipboard logic
14
+ 3. **Two buttons layout** - Both buttons at bottom of toast (not on the side)
15
+ 4. **Clean separation** - Buttons in separate component, generator in separate file
16
+
17
+ ### 🗑️ Removed: Legacy Code
18
+
19
+ **Removed Functions:**
20
+ - `copyToClipboard()` from ValidationErrorToast.tsx (replaced by `useCopy`)
21
+ - `copyCurlToClipboard()` from curl-generator.ts (replaced by `useCopy`)
22
+ - `createCopyAction()` (replaced by ValidationErrorButtons)
23
+ - `createCopyErrorAction()` (replaced by ValidationErrorButtons)
24
+ - `createCopyCurlAction()` (replaced by ValidationErrorButtons)
25
+
26
+ ### ♻️ Refactored: Cleaner API
27
+
28
+ **Before:**
29
+ ```typescript
30
+ // Multiple separate functions
31
+ createCopyAction(detail, onSuccess, onError);
32
+ createCopyErrorAction(detail, onSuccess, onError);
33
+ createCopyCurlAction(detail, onSuccess, onError);
34
+ ```
35
+
36
+ **After:**
37
+ ```typescript
38
+ // Single component with useCopy
39
+ <ValidationErrorButtons detail={detail} />
40
+ ```
41
+
42
+ ### 📦 File Structure
43
+
44
+ ```
45
+ validation/
46
+ ├── curl-generator.ts # cURL generation logic only
47
+ ├── ValidationErrorButtons.tsx # Button component (uses useCopy)
48
+ ├── ValidationErrorToast.tsx # Toast utilities (no copy logic)
49
+ ├── ValidationErrorContext.tsx # Provider & hook
50
+ ├── index.ts # Public exports
51
+ └── README.md # Documentation
52
+ ```
53
+
54
+ ## Benefits
55
+
56
+ 1. **No code duplication** - Uses existing `useCopy` instead of custom clipboard code
57
+ 2. **Consistent UX** - All copy operations show the same toast feedback
58
+ 3. **Cleaner separation** - Each file has single responsibility
59
+ 4. **Easier to maintain** - Less code, clearer structure
60
+ 5. **Better DX** - Simple `<ValidationErrorButtons />` instead of 3 separate functions
61
+
62
+ ## API Changes
63
+
64
+ ### Removed Exports
65
+ ```typescript
66
+ // ❌ No longer exported
67
+ createCopyAction
68
+ createCopyErrorAction
69
+ createCopyCurlAction
70
+ copyCurlToClipboard
71
+ ```
72
+
73
+ ### New Exports
74
+ ```typescript
75
+ // ✅ New exports
76
+ ValidationErrorButtons // Component
77
+ generateCurl // Generate cURL string
78
+ generateCurlFromError // Generate from error detail
79
+ getAuthToken // Get token from localStorage
80
+ ```
81
+
82
+ ### Unchanged Exports
83
+ ```typescript
84
+ // ✅ Still available
85
+ ValidationErrorProvider
86
+ useValidationErrors
87
+ createValidationErrorToast
88
+ formatErrorForClipboard
89
+ buildToastTitle
90
+ buildToastDescription
91
+ ```
92
+
93
+ ## Migration Guide
94
+
95
+ If you were using the old `createCopyAction`:
96
+
97
+ **Before:**
98
+ ```typescript
99
+ const toastOptions = createValidationErrorToast(errorDetail, {
100
+ onCopySuccess: () => console.log('Copied'),
101
+ onCopyError: (e) => console.error(e),
102
+ });
103
+ ```
104
+
105
+ **After:**
106
+ ```typescript
107
+ // Just use it - ValidationErrorButtons handles everything
108
+ const toastOptions = createValidationErrorToast(errorDetail);
109
+ // That's it! Toast feedback is automatic via useCopy
110
+ ```
111
+
112
+ ## Implementation Details
113
+
114
+ ### ValidationErrorButtons Component
115
+
116
+ Uses `useCopy` hook from `@djangocfg/ui`:
117
+
118
+ ```typescript
119
+ const { copyToClipboard } = useCopy();
120
+
121
+ const handleCopyError = async (e) => {
122
+ const json = JSON.stringify(errorData, null, 2);
123
+ await copyToClipboard(json, '✅ Error details copied');
124
+ };
125
+
126
+ const handleCopyCurl = async (e) => {
127
+ const curl = generateCurlFromError(detail);
128
+ await copyToClipboard(curl, '✅ cURL command copied');
129
+ };
130
+ ```
131
+
132
+ ### Auto Token Injection
133
+
134
+ ```typescript
135
+ export function generateCurl(options: CurlOptions): string {
136
+ const { token = getAuthToken() || undefined } = options;
137
+ // ...
138
+ }
139
+ ```
140
+
141
+ Token is auto-fetched if not provided.
142
+
143
+ ## Testing
144
+
145
+ 1. Trigger validation error (e.g., visit `/api/proxies/proxies/`)
146
+ 2. Toast appears with two buttons at bottom
147
+ 3. Click **📋 Copy Error** → Success toast appears → JSON in clipboard
148
+ 4. Click **🔄 Copy cURL** → Success toast appears → cURL in clipboard
149
+ 5. Paste cURL in terminal → Works with your auth token!
150
+
151
+ ## What's Next
152
+
153
+ Possible future enhancements:
154
+ - [ ] Support POST/PUT body in cURL
155
+ - [ ] Option to copy without token
156
+ - [ ] Different formats (fetch, axios)
157
+ - [ ] Copy request+response together
158
+
159
+ ---
160
+
161
+ **Date:** 2025-11-11
162
+ **Impact:** Breaking changes for anyone using internal copy functions (unlikely)
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ValidationErrorButtons Component
3
+ *
4
+ * Copy buttons for validation errors using useCopy hook
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import React from 'react';
10
+ import { Button, useCopy } from '@djangocfg/ui';
11
+ import { generateCurlFromError } from './curl-generator';
12
+ import type { ValidationErrorDetail } from './ValidationErrorToast';
13
+
14
+ export interface ValidationErrorButtonsProps {
15
+ detail: ValidationErrorDetail;
16
+ }
17
+
18
+ export function ValidationErrorButtons({ detail }: ValidationErrorButtonsProps) {
19
+ const { copyToClipboard } = useCopy();
20
+
21
+ const handleCopyError = async (e: React.MouseEvent) => {
22
+ e.preventDefault();
23
+ e.stopPropagation();
24
+
25
+ const errorData = {
26
+ timestamp: detail.timestamp.toISOString(),
27
+ operation: detail.operation,
28
+ endpoint: {
29
+ method: detail.method,
30
+ path: detail.path,
31
+ },
32
+ validation_errors: detail.error.issues.map((issue) => ({
33
+ path: issue.path.join('.') || 'root',
34
+ message: issue.message,
35
+ code: issue.code,
36
+ ...(('expected' in issue) && { expected: issue.expected }),
37
+ ...(('received' in issue) && { received: issue.received }),
38
+ })),
39
+ response: detail.response,
40
+ total_errors: detail.error.issues.length,
41
+ };
42
+
43
+ const formattedError = JSON.stringify(errorData, null, 2);
44
+ await copyToClipboard(formattedError, '✅ Error details copied');
45
+ };
46
+
47
+ const handleCopyCurl = async (e: React.MouseEvent) => {
48
+ e.preventDefault();
49
+ e.stopPropagation();
50
+
51
+ const curl = generateCurlFromError({
52
+ method: detail.method,
53
+ path: detail.path,
54
+ response: detail.response,
55
+ });
56
+
57
+ await copyToClipboard(curl, '✅ cURL command copied');
58
+ };
59
+
60
+ return (
61
+ <div className="flex gap-2 mt-2">
62
+ <Button
63
+ size="sm"
64
+ variant="outline"
65
+ onClick={handleCopyError}
66
+ className="h-8 text-xs"
67
+ >
68
+ 📋 Copy Error
69
+ </Button>
70
+ <Button
71
+ size="sm"
72
+ variant="outline"
73
+ onClick={handleCopyCurl}
74
+ className="h-8 text-xs"
75
+ >
76
+ 🔄 Copy cURL
77
+ </Button>
78
+ </div>
79
+ );
80
+ }
@@ -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
+ }