@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.
- package/package.json +5 -5
- package/src/layouts/AppLayout/AppLayout.tsx +10 -11
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +45 -59
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.example.tsx +98 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.tsx +149 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +2 -2
- package/src/layouts/AppLayout/layouts/AdminLayout/components/index.ts +2 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/context/CfgAppContext.tsx +48 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/context/index.ts +2 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +20 -12
- package/src/layouts/AppLayout/layouts/AdminLayout/index.ts +4 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/lottie/energizing.json +1 -0
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +24 -55
- package/src/layouts/UILayout/config/components/tools.config.tsx +31 -1
- package/src/validation/README.md +88 -2
- package/src/validation/REFACTORING.md +162 -0
- package/src/validation/ValidationErrorButtons.tsx +80 -0
- package/src/validation/ValidationErrorToast.tsx +7 -77
- package/src/validation/curl-generator.ts +118 -0
- package/src/validation/index.ts +12 -1
|
@@ -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
|
-
|
|
145
|
+
consola.info('[useCfgApp] parent-auth message received', { authTokenProcessed });
|
|
142
146
|
|
|
143
147
|
// Prevent processing if already handled
|
|
144
148
|
if (authTokenProcessed) {
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
|
209
|
-
if (inIframe) {
|
|
215
|
+
// Send iframe-ready ONLY ONCE (even if component remounts)
|
|
216
|
+
if (inIframe && !iframeReadySent) {
|
|
210
217
|
try {
|
|
211
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
31
|
-
import {
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 =
|
|
96
|
+
const handleSearch = (value: string) => {
|
|
92
97
|
setSearchTerm(value);
|
|
93
|
-
//
|
|
94
|
-
await refreshPayments();
|
|
98
|
+
// Client-side filtering only
|
|
95
99
|
};
|
|
96
100
|
|
|
97
|
-
const handleStatusFilter =
|
|
101
|
+
const handleStatusFilter = (status: string) => {
|
|
98
102
|
setStatusFilter(status);
|
|
99
|
-
//
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
];
|
package/src/validation/README.md
CHANGED
|
@@ -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
|
-
- **
|
|
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
|
+
}
|