@djangocfg/layouts 1.2.39 → 1.2.40
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 +16 -7
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +28 -9
- package/src/validation/README.md +145 -547
- package/src/validation/components/ErrorButtons.tsx +100 -0
- package/src/validation/components/ErrorToast.tsx +174 -0
- package/src/validation/hooks.ts +10 -0
- package/src/validation/index.ts +31 -23
- package/src/validation/providers/ErrorTrackingProvider.tsx +265 -0
- package/src/validation/types.ts +278 -0
- package/src/validation/utils/formatters.ts +114 -0
- package/src/validation/REFACTORING.md +0 -162
- package/src/validation/ValidationErrorButtons.tsx +0 -80
- package/src/validation/ValidationErrorContext.tsx +0 -333
- package/src/validation/ValidationErrorToast.tsx +0 -181
- /package/src/validation/{curl-generator.ts → utils/curl-generator.ts} +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorButtons - Universal copy buttons for all error types
|
|
3
|
+
*
|
|
4
|
+
* Provides copy functionality for validation, CORS, and network errors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { Button, useCopy } from '@djangocfg/ui';
|
|
11
|
+
import { Copy, Terminal } from 'lucide-react';
|
|
12
|
+
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail } from '../types';
|
|
13
|
+
import {
|
|
14
|
+
formatValidationErrorForClipboard,
|
|
15
|
+
formatCORSErrorForClipboard,
|
|
16
|
+
formatNetworkErrorForClipboard,
|
|
17
|
+
} from '../utils/formatters';
|
|
18
|
+
import { generateCurlFromError } from '../utils/curl-generator';
|
|
19
|
+
|
|
20
|
+
export interface ErrorButtonsProps {
|
|
21
|
+
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Format error for clipboard based on type
|
|
26
|
+
*/
|
|
27
|
+
function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail): string {
|
|
28
|
+
switch (detail.type) {
|
|
29
|
+
case 'validation':
|
|
30
|
+
return formatValidationErrorForClipboard(detail);
|
|
31
|
+
case 'cors':
|
|
32
|
+
return formatCORSErrorForClipboard(detail);
|
|
33
|
+
case 'network':
|
|
34
|
+
return formatNetworkErrorForClipboard(detail);
|
|
35
|
+
default:
|
|
36
|
+
return JSON.stringify(detail, null, 2);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if error supports cURL generation
|
|
42
|
+
*/
|
|
43
|
+
function supportsCurl(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail): boolean {
|
|
44
|
+
return detail.type === 'validation';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Universal error buttons
|
|
49
|
+
*/
|
|
50
|
+
export function ErrorButtons({ detail }: ErrorButtonsProps) {
|
|
51
|
+
const { copyToClipboard } = useCopy();
|
|
52
|
+
|
|
53
|
+
const handleCopyError = async (e: React.MouseEvent) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
|
|
57
|
+
const formattedError = formatErrorForClipboard(detail);
|
|
58
|
+
await copyToClipboard(formattedError, '✅ Error details copied');
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleCopyCurl = async (e: React.MouseEvent) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
|
|
65
|
+
if (detail.type === 'validation') {
|
|
66
|
+
const curl = generateCurlFromError({
|
|
67
|
+
method: detail.method,
|
|
68
|
+
path: detail.path,
|
|
69
|
+
response: detail.response,
|
|
70
|
+
});
|
|
71
|
+
await copyToClipboard(curl, '✅ cURL command copied');
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex gap-2 mt-2">
|
|
77
|
+
<Button
|
|
78
|
+
size="sm"
|
|
79
|
+
variant="secondary"
|
|
80
|
+
onClick={handleCopyError}
|
|
81
|
+
className="h-8 text-xs bg-background hover:bg-background/80 text-foreground border border-border gap-1.5"
|
|
82
|
+
>
|
|
83
|
+
<Copy className="h-3.5 w-3.5" />
|
|
84
|
+
Copy Error
|
|
85
|
+
</Button>
|
|
86
|
+
|
|
87
|
+
{supportsCurl(detail) && (
|
|
88
|
+
<Button
|
|
89
|
+
size="sm"
|
|
90
|
+
variant="secondary"
|
|
91
|
+
onClick={handleCopyCurl}
|
|
92
|
+
className="h-8 text-xs bg-background hover:bg-background/80 text-foreground border border-border gap-1.5"
|
|
93
|
+
>
|
|
94
|
+
<Terminal className="h-3.5 w-3.5" />
|
|
95
|
+
Copy cURL
|
|
96
|
+
</Button>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorToast - Universal toast for all error types
|
|
3
|
+
*
|
|
4
|
+
* Formats validation, CORS, and network errors with appropriate styling
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import type {
|
|
11
|
+
ValidationErrorDetail,
|
|
12
|
+
CORSErrorDetail,
|
|
13
|
+
NetworkErrorDetail,
|
|
14
|
+
ValidationErrorConfig,
|
|
15
|
+
CORSErrorConfig,
|
|
16
|
+
NetworkErrorConfig,
|
|
17
|
+
} from '../types';
|
|
18
|
+
import { formatZodIssues, formatErrorTitle, extractDomain } from '../utils/formatters';
|
|
19
|
+
import { ErrorButtons } from './ErrorButtons';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build validation error description
|
|
23
|
+
*/
|
|
24
|
+
function buildValidationDescription(
|
|
25
|
+
detail: ValidationErrorDetail,
|
|
26
|
+
config: Required<ValidationErrorConfig>
|
|
27
|
+
): React.ReactNode {
|
|
28
|
+
const descriptionParts: string[] = [];
|
|
29
|
+
|
|
30
|
+
// Add HTTP method and path
|
|
31
|
+
if (config.showPath) {
|
|
32
|
+
descriptionParts.push(`${detail.method} ${detail.path}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add error count
|
|
36
|
+
if (config.showErrorCount) {
|
|
37
|
+
const count = detail.error.issues.length;
|
|
38
|
+
const plural = count === 1 ? 'error' : 'errors';
|
|
39
|
+
descriptionParts.push(`${count} ${plural}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add formatted error messages
|
|
43
|
+
const issuesText = formatZodIssues(detail.error, config.maxIssuesInToast);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex flex-col gap-2 text-sm">
|
|
47
|
+
{descriptionParts.length > 0 && (
|
|
48
|
+
<div className="font-mono text-xs opacity-90">
|
|
49
|
+
{descriptionParts.join(' • ')}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
<div className="opacity-90">{issuesText}</div>
|
|
53
|
+
<ErrorButtons detail={detail} />
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build CORS error description
|
|
60
|
+
*/
|
|
61
|
+
function buildCORSDescription(
|
|
62
|
+
detail: CORSErrorDetail,
|
|
63
|
+
config: Required<CORSErrorConfig>
|
|
64
|
+
): React.ReactNode {
|
|
65
|
+
const domain = extractDomain(detail.url);
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
|
|
68
|
+
// Add method and URL info
|
|
69
|
+
if (config.showMethod && config.showUrl) {
|
|
70
|
+
parts.push(`${detail.method} ${detail.url}`);
|
|
71
|
+
} else if (config.showUrl) {
|
|
72
|
+
parts.push(detail.url);
|
|
73
|
+
} else if (config.showMethod) {
|
|
74
|
+
parts.push(`${detail.method} request blocked`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex flex-col gap-2 text-sm">
|
|
79
|
+
{parts.length > 0 && (
|
|
80
|
+
<div className="font-mono text-xs opacity-90">
|
|
81
|
+
{parts.join(' • ')}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<div className="flex flex-col gap-1">
|
|
86
|
+
<div className="font-medium">Сервер заблокировал запрос из-за CORS политики</div>
|
|
87
|
+
|
|
88
|
+
<div className="text-xs opacity-75 mt-1">
|
|
89
|
+
<div className="font-semibold mb-1">Возможные причины:</div>
|
|
90
|
+
<ul className="list-disc list-inside space-y-0.5 ml-2">
|
|
91
|
+
<li>Сервер не настроен для cross-origin запросов</li>
|
|
92
|
+
<li>Отсутствует заголовок Access-Control-Allow-Origin</li>
|
|
93
|
+
<li>Неверные CORS настройки на {domain}</li>
|
|
94
|
+
</ul>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="text-xs opacity-75 mt-2">
|
|
98
|
+
<div className="font-semibold mb-1">Что делать:</div>
|
|
99
|
+
<ul className="list-disc list-inside space-y-0.5 ml-2">
|
|
100
|
+
<li>Проверьте CORS настройки Django (CORS_ALLOWED_ORIGINS)</li>
|
|
101
|
+
<li>Убедитесь что django-cors-headers установлен</li>
|
|
102
|
+
<li>Проверьте что сервер доступен</li>
|
|
103
|
+
</ul>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<ErrorButtons detail={detail} />
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build network error description
|
|
114
|
+
*/
|
|
115
|
+
function buildNetworkDescription(
|
|
116
|
+
detail: NetworkErrorDetail,
|
|
117
|
+
config: Required<NetworkErrorConfig>
|
|
118
|
+
): React.ReactNode {
|
|
119
|
+
const parts: string[] = [];
|
|
120
|
+
|
|
121
|
+
// Add method and URL info
|
|
122
|
+
if (config.showMethod && config.showUrl) {
|
|
123
|
+
parts.push(`${detail.method} ${detail.url}`);
|
|
124
|
+
} else if (config.showUrl) {
|
|
125
|
+
parts.push(detail.url);
|
|
126
|
+
} else if (config.showMethod) {
|
|
127
|
+
parts.push(`${detail.method} request failed`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Add status code
|
|
131
|
+
if (config.showStatusCode && detail.statusCode) {
|
|
132
|
+
parts.push(`Status: ${detail.statusCode}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="flex flex-col gap-2 text-sm">
|
|
137
|
+
{parts.length > 0 && (
|
|
138
|
+
<div className="font-mono text-xs opacity-90">
|
|
139
|
+
{parts.join(' • ')}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<div className="opacity-90">{detail.error}</div>
|
|
144
|
+
|
|
145
|
+
<ErrorButtons detail={detail} />
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create toast options for any error type
|
|
152
|
+
*/
|
|
153
|
+
export function createErrorToast(
|
|
154
|
+
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail,
|
|
155
|
+
config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig>
|
|
156
|
+
) {
|
|
157
|
+
let description: React.ReactNode;
|
|
158
|
+
|
|
159
|
+
// Build description based on error type
|
|
160
|
+
if (detail.type === 'validation') {
|
|
161
|
+
description = buildValidationDescription(detail, config as Required<ValidationErrorConfig>);
|
|
162
|
+
} else if (detail.type === 'cors') {
|
|
163
|
+
description = buildCORSDescription(detail, config as Required<CORSErrorConfig>);
|
|
164
|
+
} else {
|
|
165
|
+
description = buildNetworkDescription(detail, config as Required<NetworkErrorConfig>);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
title: formatErrorTitle(detail),
|
|
170
|
+
description,
|
|
171
|
+
variant: 'destructive' as const,
|
|
172
|
+
duration: config.duration,
|
|
173
|
+
};
|
|
174
|
+
}
|
package/src/validation/index.ts
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Error Tracking - Unified error tracking for all error types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* from API client using browser CustomEvent API.
|
|
4
|
+
* Single provider and hook for validation, CORS, and network errors
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type ValidationErrorDetail,
|
|
12
|
-
type StoredValidationError,
|
|
13
|
-
type ValidationErrorConfig,
|
|
14
|
-
type ValidationErrorContextValue,
|
|
15
|
-
} from './ValidationErrorContext';
|
|
7
|
+
// Main provider and hook
|
|
8
|
+
export { ErrorTrackingProvider } from './providers/ErrorTrackingProvider';
|
|
9
|
+
export { useErrors } from './hooks';
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
// Types
|
|
12
|
+
export type {
|
|
13
|
+
ErrorDetail,
|
|
14
|
+
ValidationErrorDetail,
|
|
15
|
+
CORSErrorDetail,
|
|
16
|
+
NetworkErrorDetail,
|
|
17
|
+
StoredError,
|
|
18
|
+
ErrorTrackingConfig,
|
|
19
|
+
ValidationErrorConfig,
|
|
20
|
+
CORSErrorConfig,
|
|
21
|
+
NetworkErrorConfig,
|
|
22
|
+
ErrorTrackingContextValue,
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
// Components
|
|
26
|
+
export { ErrorButtons } from './components/ErrorButtons';
|
|
27
|
+
export { createErrorToast } from './components/ErrorToast';
|
|
25
28
|
|
|
29
|
+
// Utilities
|
|
26
30
|
export {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
formatZodIssues,
|
|
32
|
+
formatValidationErrorForClipboard,
|
|
33
|
+
formatCORSErrorForClipboard,
|
|
34
|
+
formatNetworkErrorForClipboard,
|
|
35
|
+
formatErrorTitle,
|
|
36
|
+
extractDomain,
|
|
37
|
+
} from './utils/formatters';
|
|
30
38
|
|
|
31
39
|
export {
|
|
32
40
|
generateCurl,
|
|
33
41
|
generateCurlFromError,
|
|
34
42
|
getAuthToken,
|
|
35
43
|
type CurlOptions,
|
|
36
|
-
} from './curl-generator';
|
|
44
|
+
} from './utils/curl-generator';
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorTrackingProvider - Universal error tracking
|
|
3
|
+
*
|
|
4
|
+
* Single provider that tracks all error types:
|
|
5
|
+
* - Validation errors (Zod)
|
|
6
|
+
* - CORS errors
|
|
7
|
+
* - Network errors
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // Default - all enabled
|
|
12
|
+
* <ErrorTrackingProvider>
|
|
13
|
+
* <App />
|
|
14
|
+
* </ErrorTrackingProvider>
|
|
15
|
+
*
|
|
16
|
+
* // Custom configuration
|
|
17
|
+
* <ErrorTrackingProvider
|
|
18
|
+
* validation={{ showToast: true, maxErrors: 100 }}
|
|
19
|
+
* cors={{ enabled: true }}
|
|
20
|
+
* network={{ enabled: false }}
|
|
21
|
+
* >
|
|
22
|
+
* <App />
|
|
23
|
+
* </ErrorTrackingProvider>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use client';
|
|
28
|
+
|
|
29
|
+
import React, { createContext, useContext, useCallback, useEffect, useState, ReactNode } from 'react';
|
|
30
|
+
import { toast } from '@djangocfg/ui';
|
|
31
|
+
import type {
|
|
32
|
+
ErrorDetail,
|
|
33
|
+
StoredError,
|
|
34
|
+
ErrorTrackingConfig,
|
|
35
|
+
ValidationErrorConfig,
|
|
36
|
+
CORSErrorConfig,
|
|
37
|
+
NetworkErrorConfig,
|
|
38
|
+
ValidationErrorDetail,
|
|
39
|
+
CORSErrorDetail,
|
|
40
|
+
NetworkErrorDetail,
|
|
41
|
+
ErrorTrackingContextValue,
|
|
42
|
+
} from '../types';
|
|
43
|
+
import {
|
|
44
|
+
DEFAULT_VALIDATION_CONFIG,
|
|
45
|
+
DEFAULT_CORS_CONFIG,
|
|
46
|
+
DEFAULT_NETWORK_CONFIG,
|
|
47
|
+
ERROR_EVENTS,
|
|
48
|
+
} from '../types';
|
|
49
|
+
import { createErrorToast } from '../components/ErrorToast';
|
|
50
|
+
|
|
51
|
+
const ErrorTrackingContext = createContext<ErrorTrackingContextValue | undefined>(undefined);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate unique ID for error
|
|
55
|
+
*/
|
|
56
|
+
let errorIdCounter = 0;
|
|
57
|
+
function generateErrorId(type: string): string {
|
|
58
|
+
return `${type}-error-${Date.now()}-${++errorIdCounter}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ErrorTrackingProviderProps {
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
validation?: Partial<ValidationErrorConfig>;
|
|
64
|
+
cors?: Partial<CORSErrorConfig>;
|
|
65
|
+
network?: Partial<NetworkErrorConfig>;
|
|
66
|
+
onError?: (error: ErrorDetail) => boolean | void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Universal Error Tracking Provider
|
|
71
|
+
*
|
|
72
|
+
* Tracks all error types with a single provider
|
|
73
|
+
*/
|
|
74
|
+
export function ErrorTrackingProvider({
|
|
75
|
+
children,
|
|
76
|
+
validation: userValidationConfig,
|
|
77
|
+
cors: userCorsConfig,
|
|
78
|
+
network: userNetworkConfig,
|
|
79
|
+
onError,
|
|
80
|
+
}: ErrorTrackingProviderProps) {
|
|
81
|
+
const [errors, setErrors] = useState<StoredError[]>([]);
|
|
82
|
+
|
|
83
|
+
// Merge user configs with defaults
|
|
84
|
+
const validationConfig: Required<ValidationErrorConfig> = {
|
|
85
|
+
...DEFAULT_VALIDATION_CONFIG,
|
|
86
|
+
...userValidationConfig,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const corsConfig: Required<CORSErrorConfig> = {
|
|
90
|
+
...DEFAULT_CORS_CONFIG,
|
|
91
|
+
...userCorsConfig,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const networkConfig: Required<NetworkErrorConfig> = {
|
|
95
|
+
...DEFAULT_NETWORK_CONFIG,
|
|
96
|
+
...userNetworkConfig,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear all errors
|
|
101
|
+
*/
|
|
102
|
+
const clearErrors = useCallback(() => {
|
|
103
|
+
setErrors([]);
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clear errors by type
|
|
108
|
+
*/
|
|
109
|
+
const clearErrorsByType = useCallback((type: 'validation' | 'cors' | 'network') => {
|
|
110
|
+
setErrors((prev) => prev.filter((error) => error.type !== type));
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Clear specific error
|
|
115
|
+
*/
|
|
116
|
+
const clearError = useCallback((id: string) => {
|
|
117
|
+
setErrors((prev) => prev.filter((error) => error.id !== id));
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handle any error event
|
|
122
|
+
*/
|
|
123
|
+
const handleError = useCallback(
|
|
124
|
+
(detail: ErrorDetail, config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig>) => {
|
|
125
|
+
// Create stored error with ID
|
|
126
|
+
const storedError: StoredError = {
|
|
127
|
+
...detail,
|
|
128
|
+
id: generateErrorId(detail.type),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Add to errors array (limited by maxErrors)
|
|
132
|
+
setErrors((prev) => {
|
|
133
|
+
const updated = [storedError, ...prev];
|
|
134
|
+
return updated.slice(0, config.maxErrors);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Call custom error handler
|
|
138
|
+
const shouldShowToast = onError?.(detail) !== false;
|
|
139
|
+
|
|
140
|
+
// Show toast notification
|
|
141
|
+
if (config.showToast && shouldShowToast) {
|
|
142
|
+
const toastOptions = createErrorToast(detail, config);
|
|
143
|
+
toast(toastOptions);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[onError]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Setup event listeners
|
|
151
|
+
*/
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
// Only run in browser
|
|
154
|
+
if (typeof window === 'undefined') return;
|
|
155
|
+
|
|
156
|
+
const handlers: Array<{ event: string; handler: (e: Event) => void }> = [];
|
|
157
|
+
|
|
158
|
+
// Validation errors
|
|
159
|
+
if (validationConfig.enabled) {
|
|
160
|
+
const handler = (event: Event) => {
|
|
161
|
+
if (!(event instanceof CustomEvent)) return;
|
|
162
|
+
const detail: ValidationErrorDetail = {
|
|
163
|
+
...event.detail,
|
|
164
|
+
type: 'validation' as const,
|
|
165
|
+
};
|
|
166
|
+
handleError(detail, validationConfig);
|
|
167
|
+
};
|
|
168
|
+
window.addEventListener(ERROR_EVENTS.VALIDATION, handler);
|
|
169
|
+
handlers.push({ event: ERROR_EVENTS.VALIDATION, handler });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// CORS errors
|
|
173
|
+
if (corsConfig.enabled) {
|
|
174
|
+
const handler = (event: Event) => {
|
|
175
|
+
if (!(event instanceof CustomEvent)) return;
|
|
176
|
+
const detail: CORSErrorDetail = {
|
|
177
|
+
...event.detail,
|
|
178
|
+
type: 'cors' as const,
|
|
179
|
+
};
|
|
180
|
+
handleError(detail, corsConfig);
|
|
181
|
+
};
|
|
182
|
+
window.addEventListener(ERROR_EVENTS.CORS, handler);
|
|
183
|
+
handlers.push({ event: ERROR_EVENTS.CORS, handler });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Network errors
|
|
187
|
+
if (networkConfig.enabled) {
|
|
188
|
+
const handler = (event: Event) => {
|
|
189
|
+
if (!(event instanceof CustomEvent)) return;
|
|
190
|
+
const detail: NetworkErrorDetail = {
|
|
191
|
+
...event.detail,
|
|
192
|
+
type: 'network' as const,
|
|
193
|
+
};
|
|
194
|
+
handleError(detail, networkConfig);
|
|
195
|
+
};
|
|
196
|
+
window.addEventListener(ERROR_EVENTS.NETWORK, handler);
|
|
197
|
+
handlers.push({ event: ERROR_EVENTS.NETWORK, handler });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Cleanup
|
|
201
|
+
return () => {
|
|
202
|
+
handlers.forEach(({ event, handler }) => {
|
|
203
|
+
window.removeEventListener(event, handler);
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
}, [handleError, validationConfig, corsConfig, networkConfig]);
|
|
207
|
+
|
|
208
|
+
// Filter errors by type
|
|
209
|
+
const validationErrors = errors.filter((e) => e.type === 'validation') as StoredError<ValidationErrorDetail>[];
|
|
210
|
+
const corsErrors = errors.filter((e) => e.type === 'cors') as StoredError<CORSErrorDetail>[];
|
|
211
|
+
const networkErrors = errors.filter((e) => e.type === 'network') as StoredError<NetworkErrorDetail>[];
|
|
212
|
+
|
|
213
|
+
const value: ErrorTrackingContextValue = {
|
|
214
|
+
errors,
|
|
215
|
+
validationErrors,
|
|
216
|
+
corsErrors,
|
|
217
|
+
networkErrors,
|
|
218
|
+
clearErrors,
|
|
219
|
+
clearErrorsByType,
|
|
220
|
+
clearError,
|
|
221
|
+
errorCount: errors.length,
|
|
222
|
+
latestError: errors[0] || null,
|
|
223
|
+
config: {
|
|
224
|
+
validation: validationConfig,
|
|
225
|
+
cors: corsConfig,
|
|
226
|
+
network: networkConfig,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<ErrorTrackingContext.Provider value={value}>
|
|
232
|
+
{children}
|
|
233
|
+
</ErrorTrackingContext.Provider>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* useErrors Hook
|
|
239
|
+
*
|
|
240
|
+
* Access errors from any component
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```tsx
|
|
244
|
+
* function ErrorPanel() {
|
|
245
|
+
* const { errors, validationErrors, clearErrors } = useErrors();
|
|
246
|
+
*
|
|
247
|
+
* return (
|
|
248
|
+
* <div>
|
|
249
|
+
* <h3>Errors ({errors.length})</h3>
|
|
250
|
+
* <h4>Validation: {validationErrors.length}</h4>
|
|
251
|
+
* <button onClick={clearErrors}>Clear All</button>
|
|
252
|
+
* </div>
|
|
253
|
+
* );
|
|
254
|
+
* }
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export function useErrors(): ErrorTrackingContextValue {
|
|
258
|
+
const context = useContext(ErrorTrackingContext);
|
|
259
|
+
|
|
260
|
+
if (context === undefined) {
|
|
261
|
+
throw new Error('useErrors must be used within ErrorTrackingProvider');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return context;
|
|
265
|
+
}
|