@djangocfg/layouts 1.2.33 → 1.2.35

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.
@@ -8,6 +8,7 @@ import React from 'react';
8
8
  import { useState, useEffect } from 'react';
9
9
  import { useRouter } from 'next/router';
10
10
  import { authLogger } from '../../../../../utils/logger';
11
+ import { consola } from 'consola';
11
12
 
12
13
  export interface UseCfgAppReturn {
13
14
  /**
@@ -86,6 +87,9 @@ export interface UseCfgAppOptions {
86
87
  * );
87
88
  * ```
88
89
  */
90
+ // Global flag to track if iframe-ready was sent (persists across component remounts)
91
+ let iframeReadySent = false;
92
+
89
93
  export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
90
94
  const router = useRouter();
91
95
  const [isMounted, setIsMounted] = useState(false);
@@ -138,11 +142,11 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
138
142
 
139
143
  switch (type) {
140
144
  case 'parent-auth':
141
- console.log('[useCfgApp] parent-auth message received, authTokenProcessed:', authTokenProcessed);
145
+ consola.info('[useCfgApp] parent-auth message received', { authTokenProcessed });
142
146
 
143
147
  // Prevent processing if already handled
144
148
  if (authTokenProcessed) {
145
- console.log('[useCfgApp] Auth tokens already processed, ignoring duplicate message');
149
+ consola.warn('[useCfgApp] Auth tokens already processed, ignoring duplicate message');
146
150
  return;
147
151
  }
148
152
 
@@ -155,21 +159,24 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
155
159
  authTokenTimeout = setTimeout(() => {
156
160
  // Double-check still not processed (race condition protection)
157
161
  if (authTokenProcessed) {
158
- console.log('[useCfgApp] Auth tokens already processed during debounce, skipping');
162
+ consola.warn('[useCfgApp] Auth tokens already processed during debounce, skipping');
159
163
  return;
160
164
  }
161
165
 
162
166
  // Receive authentication tokens from parent
163
167
  if (data?.authToken && callbackRef.current) {
164
- console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
165
- console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
168
+ consola.success('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
169
+ consola.debug('[useCfgApp] Tokens:', {
170
+ authToken: data.authToken.substring(0, 20) + '...',
171
+ refreshToken: data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null'
172
+ });
166
173
 
167
174
  // Mark as processed BEFORE calling callback
168
175
  authTokenProcessed = true;
169
176
 
170
177
  try {
171
178
  callbackRef.current(data.authToken, data.refreshToken);
172
- console.log('[useCfgApp] onAuthTokenReceived callback completed successfully');
179
+ consola.success('[useCfgApp] onAuthTokenReceived callback completed successfully');
173
180
  } catch (e) {
174
181
  authLogger.error('Failed to process auth tokens:', e);
175
182
  // Reset on error to allow retry
@@ -205,10 +212,10 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
205
212
  window.addEventListener('message', handleMessage);
206
213
  // console.log('[useCfgApp] Message listener registered, isEmbedded:', inIframe);
207
214
 
208
- // Send iframe-ready since listener is registered
209
- if (inIframe) {
215
+ // Send iframe-ready ONLY ONCE (even if component remounts)
216
+ if (inIframe && !iframeReadySent) {
210
217
  try {
211
- // console.log('[useCfgApp] Sending iframe-ready message to parent');
218
+ consola.start('[useCfgApp] Sending iframe-ready message to parent (FIRST TIME)');
212
219
  window.parent.postMessage({
213
220
  type: 'iframe-ready',
214
221
  data: {
@@ -216,12 +223,13 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
216
223
  referrer: document.referrer
217
224
  }
218
225
  }, '*');
219
- // authLogger.debug('iframe-ready message sent');
226
+ iframeReadySent = true; // Mark as sent to prevent duplicates
227
+ consola.success('[useCfgApp] iframe-ready message sent');
220
228
  } catch (e) {
221
229
  authLogger.error('Failed to notify parent about ready state:', e);
222
230
  }
223
- } else {
224
- // console.log('[useCfgApp] Not in iframe, skipping iframe-ready message');
231
+ } else if (inIframe && iframeReadySent) {
232
+ consola.info('[useCfgApp] iframe-ready already sent, skipping to prevent loop');
225
233
  }
226
234
 
227
235
  return () => {
@@ -13,6 +13,10 @@ export type { AdminLayoutProps } from './AdminLayout';
13
13
  export { useCfgApp, useApp } from './hooks';
14
14
  export type { UseCfgAppReturn, UseCfgAppOptions, UseAppReturn, UseAppOptions } from './hooks';
15
15
 
16
+ // Context
17
+ export { CfgAppProvider, useCfgAppContext } from './context';
18
+ export type { CfgAppProviderProps } from './context';
19
+
16
20
  // Components
17
21
  export { ParentSync, AuthStatusSync } from './components';
18
22
 
@@ -0,0 +1 @@
1
+ {"nm":"Rander","ddd":0,"h":512,"w":512,"meta":{"g":"LottieFiles AE 2.0.4"},"layers":[{"ty":4,"nm":"Capa 2","sr":1,"st":0,"op":46,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[256.002,256,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.956,"y":0},"i":{"x":0.237,"y":1},"s":[120,120,100],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.395,"y":1},"s":[60,60,100],"t":40},{"s":[120,120,100],"t":46}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256.002,256,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[1.638,-9.168],[0,0],[0,0],[-8.437,0],[0,0],[-5.944,5.985],[0,0]],"o":[[0,0],[0,0],[-5.944,5.985],[0,0],[-8.437,0],[0,0],[6.562,-6.613]],"v":[[75.715,-65.189],[73.984,-55.526],[-40.87,60.214],[-34.133,76.388],[-66.367,76.388],[-73.104,60.214],[59.634,-73.544]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.8196,0.3569],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[226.809,194.352],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[1.061,-5.831],[0,0],[0,0],[-1.679,9.179],[0,0],[5.924,0],[0,0]],"o":[[0,0],[0,0],[-6.562,6.624],[0,0],[1.061,-5.831],[0,0],[5.934,0]],"v":[[25.334,-50.448],[8.686,40.722],[-9.249,58.802],[-25.32,50.416],[-6.9,-50.448],[-16.244,-61.646],[15.99,-61.646]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.8196,0.3569],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[233.82,332.386],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tr","a":{"a":0,"k":[233.82,332.386],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[233.82,332.386],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[6.558,-6.608],[0,0],[-8.439,0],[0,0],[1.064,-5.829],[0,0],[-6.57,6.624],[0,0],[8.2,0.413],[0,0],[-1.005,5.629],[0,0]],"o":[[0,0],[-5.945,5.99],[0,0],[5.925,0],[0,0],[-1.676,9.178],[0,0],[5.782,-5.829],[0,0],[-5.71,-0.288],[0,0],[1.639,-9.166]],"v":[[30.44,-135.194],[-102.297,-1.434],[-95.559,14.745],[-38.427,14.745],[-29.089,25.943],[-47.507,126.802],[-31.43,135.192],[102.301,0.367],[96.039,-15.798],[38.063,-18.72],[29.196,-29.87],[46.521,-126.837]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9961,0.8941,0.3529],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[256.002,256],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Shape Layer 1","sr":1,"st":-41,"op":46,"ip":-39,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[6,22,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0,0,100],"t":13},{"s":[800,800,100],"t":30}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[252.5,252.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":13},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":23},{"s":[0],"t":30}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[30,30],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":13},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[3],"t":23},{"s":[0],"t":30}],"ix":5},"c":{"a":0,"k":[1,0.9647,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[6,22],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2}],"v":"4.8.0","fr":30,"op":46,"ip":0,"assets":[]}
@@ -26,21 +26,26 @@ import {
26
26
  SelectTrigger,
27
27
  SelectValue,
28
28
  Skeleton,
29
+ useDRFPagination,
30
+ StaticPagination,
29
31
  } from '@djangocfg/ui';
30
- import { Plus, Search, Filter, ChevronLeft, ChevronRight, RefreshCw, ExternalLink } from 'lucide-react';
31
- import { usePaymentsContext } from '@djangocfg/api/cfg/contexts';
32
+ import { Plus, Search, Filter, RefreshCw, ExternalLink } from 'lucide-react';
33
+ import { api, Hooks } from '@djangocfg/api';
32
34
  import { openCreatePaymentDialog, openPaymentDetailsDialog } from '../../../events';
33
35
 
34
36
  export const PaymentsList: React.FC = () => {
37
+ // Local pagination state
38
+ const pagination = useDRFPagination(1, 20);
39
+
40
+ // Fetch payments with pagination
35
41
  const {
36
- payments,
37
- isLoadingPayments,
38
- refreshPayments,
39
- } = usePaymentsContext();
42
+ data: payments,
43
+ error,
44
+ isLoading: isLoadingPayments,
45
+ mutate: refreshPayments,
46
+ } = Hooks.usePaymentsPaymentsList(pagination.params, api as any);
40
47
 
41
48
  const paymentsList = payments?.results || [];
42
- const currentPage = payments?.page || 1;
43
- const pageSize = payments?.page_size || 20;
44
49
  const totalCount = payments?.count || 0;
45
50
 
46
51
  const [searchTerm, setSearchTerm] = useState('');
@@ -88,22 +93,16 @@ export const PaymentsList: React.FC = () => {
88
93
  }
89
94
  };
90
95
 
91
- const handleSearch = async (value: string) => {
96
+ const handleSearch = (value: string) => {
92
97
  setSearchTerm(value);
93
- // TODO: Implement search/filter in PaymentsContext when API supports it
94
- await refreshPayments();
98
+ // Client-side filtering only
95
99
  };
96
100
 
97
- const handleStatusFilter = async (status: string) => {
101
+ const handleStatusFilter = (status: string) => {
98
102
  setStatusFilter(status);
99
- // TODO: Implement status filter in PaymentsContext when API supports it
100
- await refreshPayments();
103
+ // Client-side filtering only
101
104
  };
102
105
 
103
- const handlePageChange = async (page: number) => {
104
- // TODO: Implement pagination in PaymentsContext
105
- await refreshPayments();
106
- };
107
106
 
108
107
  // Filter payments client-side for now (until API supports filtering)
109
108
  const filteredPayments = paymentsList.filter((payment) => {
@@ -120,17 +119,13 @@ export const PaymentsList: React.FC = () => {
120
119
  return matchesSearch && matchesStatus;
121
120
  });
122
121
 
123
- const totalPages = Math.ceil((totalCount || 0) / (pageSize || 20));
124
- const showingFrom = ((currentPage || 1) - 1) * (pageSize || 20) + 1;
125
- const showingTo = Math.min((currentPage || 1) * (pageSize || 20), totalCount || 0);
126
-
127
122
  return (
128
123
  <Card>
129
124
  <CardHeader>
130
125
  <CardTitle className="flex items-center justify-between">
131
126
  <span>Payment History</span>
132
127
  <div className="flex items-center gap-2">
133
- <Button variant="outline" size="sm" onClick={refreshPayments} disabled={isLoadingPayments}>
128
+ <Button variant="outline" size="sm" onClick={() => refreshPayments()} disabled={isLoadingPayments}>
134
129
  <RefreshCw className={`h-4 w-4 mr-2 ${isLoadingPayments ? 'animate-spin' : ''}`} />
135
130
  Refresh
136
131
  </Button>
@@ -267,38 +262,12 @@ export const PaymentsList: React.FC = () => {
267
262
  </Table>
268
263
  </div>
269
264
 
270
- {/* Pagination */}
271
- {totalPages > 1 && (
272
- <div className="flex items-center justify-between">
273
- <div className="text-sm text-muted-foreground">
274
- Showing {showingFrom} to {showingTo} of {totalCount} payments
275
- </div>
276
-
277
- <div className="flex items-center gap-2">
278
- <Button
279
- variant="outline"
280
- size="sm"
281
- onClick={() => handlePageChange((currentPage || 1) - 1)}
282
- disabled={!currentPage || currentPage <= 1}
283
- >
284
- <ChevronLeft className="h-4 w-4" />
285
- </Button>
286
-
287
- <span className="text-sm">
288
- Page {currentPage || 1} of {totalPages}
289
- </span>
290
-
291
- <Button
292
- variant="outline"
293
- size="sm"
294
- onClick={() => handlePageChange((currentPage || 1) + 1)}
295
- disabled={!currentPage || currentPage >= totalPages}
296
- >
297
- <ChevronRight className="h-4 w-4" />
298
- </Button>
299
- </div>
300
- </div>
301
- )}
265
+ {/* DRF Pagination */}
266
+ <StaticPagination
267
+ data={payments}
268
+ onPageChange={pagination.setPage}
269
+ className="mt-4"
270
+ />
302
271
  </>
303
272
  )}
304
273
  </CardContent>
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import React from 'react';
6
- import { JsonTree, PrettyCode, Mermaid } from '@djangocfg/ui';
6
+ import { JsonTree, PrettyCode, Mermaid, LottiePlayer } from '@djangocfg/ui';
7
7
  import type { ComponentConfig } from './types';
8
8
 
9
9
  // Sample data for demos
@@ -201,4 +201,34 @@ export const TOOLS_COMPONENTS: ComponentConfig[] = [
201
201
  </div>
202
202
  ),
203
203
  },
204
+ {
205
+ name: 'LottiePlayer',
206
+ category: 'tools',
207
+ description: 'Lottie animation player with size presets and playback controls',
208
+ importPath: `import { LottiePlayer } from '@djangocfg/ui';`,
209
+ example: `<LottiePlayer
210
+ src="https://lottie.host/embed/a0eb3923-2f93-4a2e-9c91-3e0b0f6f3b3e/WHJEbMDJLn.json"
211
+ size="md"
212
+ autoplay
213
+ loop
214
+ />
215
+
216
+ // Custom size and speed
217
+ <LottiePlayer
218
+ src={animationData}
219
+ width={300}
220
+ height={300}
221
+ speed={1.5}
222
+ />`,
223
+ preview: (
224
+ <div className="flex items-center justify-center p-8">
225
+ <LottiePlayer
226
+ src="https://lottie.host/embed/a0eb3923-2f93-4a2e-9c91-3e0b0f6f3b3e/WHJEbMDJLn.json"
227
+ size="md"
228
+ autoplay
229
+ loop
230
+ />
231
+ </div>
232
+ ),
233
+ },
204
234
  ];
@@ -8,7 +8,8 @@ Automatic Zod validation error tracking with toast notifications and copy-to-cli
8
8
 
9
9
  ✅ **Auto-capture validation errors** from API client via CustomEvent
10
10
  ✅ **Toast notifications** with destructive styling
11
- ✅ **Copy button** - One-click copy full error details to clipboard
11
+ ✅ **Copy Error button** - One-click copy full error details JSON to clipboard
12
+ ✅ **Copy cURL button** - Generate and copy cURL command with auth token
12
13
  ✅ **Error history** - Store and manage validation errors
13
14
  ✅ **Configurable** - Customize display and behavior
14
15
  ✅ **Type-safe** - Full TypeScript support
@@ -69,7 +70,9 @@ window.dispatchEvent(new CustomEvent('zod-validation-error', {
69
70
  You'll see a toast with:
70
71
  - **Title**: "❌ Validation Error in operation_name"
71
72
  - **Description**: Endpoint, error count, first 3 errors
72
- - **Copy Button**: "📋 Copy" - Copies full error JSON to clipboard
73
+ - **Two Buttons** (displayed at bottom):
74
+ - **📋 Copy Error** - Copies full error JSON to clipboard
75
+ - **🔄 Copy cURL** - Generates and copies cURL command with auth token
73
76
 
74
77
  ---
75
78
 
@@ -490,8 +493,91 @@ validation/
490
493
 
491
494
  ---
492
495
 
496
+ ## Copy as cURL Feature
497
+
498
+ ### What Gets Copied
499
+
500
+ When you click **🔄 Copy cURL**, a ready-to-use cURL command is generated:
501
+
502
+ ```bash
503
+ curl 'http://localhost:8000/api/proxies/proxies/?page=1&page_size=100' \
504
+ -H 'Accept: */*' \
505
+ -H 'Content-Type: application/json' \
506
+ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
507
+ ```
508
+
509
+ The generator automatically:
510
+ - ✅ Adds your auth token from localStorage
511
+ - ✅ Formats headers properly
512
+ - ✅ Uses correct HTTP method
513
+ - ✅ Escapes special characters
514
+
515
+ ### Token Auto-Detection
516
+
517
+ The cURL generator looks for your token in localStorage:
518
+ - `access_token` (default)
519
+ - `token`
520
+ - `auth_token`
521
+
522
+ ### Manual cURL Generation
523
+
524
+ You can also generate cURL commands programmatically:
525
+
526
+ ```typescript
527
+ import { generateCurl, copyCurlToClipboard } from '@djangocfg/layouts/validation';
528
+
529
+ // Generate cURL
530
+ const curl = generateCurl({
531
+ method: 'POST',
532
+ path: '/api/users/',
533
+ token: 'your-token',
534
+ body: { name: 'John' },
535
+ headers: { 'X-Custom': 'value' },
536
+ });
537
+
538
+ // Copy to clipboard
539
+ await copyCurlToClipboard(curl);
540
+ ```
541
+
542
+ ### API Reference - cURL
543
+
544
+ #### `generateCurl(options: CurlOptions): string`
545
+
546
+ **Options:**
547
+ ```typescript
548
+ interface CurlOptions {
549
+ method: string; // HTTP method
550
+ path: string; // API path
551
+ token?: string; // Auth token (auto-fetched if omitted)
552
+ body?: any; // Request body
553
+ headers?: Record<string, string>; // Custom headers
554
+ baseUrl?: string; // API base URL (from env by default)
555
+ }
556
+ ```
557
+
558
+ #### `generateCurlFromError(detail): string`
559
+
560
+ Generate cURL from validation error details. Auto-fetches token from localStorage.
561
+
562
+ #### `getAuthToken(): string | null`
563
+
564
+ Get authentication token from localStorage.
565
+
566
+ #### `copyCurlToClipboard(curl: string): Promise<boolean>`
567
+
568
+ Copy cURL command to clipboard. Returns `true` on success.
569
+
570
+ ---
571
+
493
572
  ## Changelog
494
573
 
574
+ ### 2025-11-11 - v2.1.0
575
+ - ✨ Added **Copy cURL button** with automatic token injection
576
+ - ✨ Added `curl-generator.ts` utilities
577
+ - ✨ Added two buttons layout at bottom of toast
578
+ - ✨ Auto-detect auth token from localStorage
579
+ - 📝 Updated documentation with cURL examples
580
+
495
581
  ### 2025-11-11 - v2.0.0
496
582
  - ✨ Added copy-to-clipboard button in toast
497
583
  - ✨ Added `ValidationErrorToast` utilities
@@ -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
+ }