@djangocfg/layouts 2.1.541 → 2.1.542
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/README.md +1 -1
- package/package.json +17 -17
- package/src/components/core/JsonLd.tsx +1 -1
- package/src/components/errors/ErrorsTracker/components/ErrorButtons.tsx +4 -7
- package/src/components/errors/ErrorsTracker/components/ErrorToast.tsx +3 -41
- package/src/components/errors/ErrorsTracker/index.ts +1 -4
- package/src/components/errors/ErrorsTracker/providers/ErrorTrackingProvider.tsx +40 -59
- package/src/components/errors/ErrorsTracker/types.ts +2 -53
- package/src/components/errors/ErrorsTracker/utils/curl-generator.ts +2 -2
- package/src/components/errors/ErrorsTracker/utils/formatters.ts +2 -29
- package/src/components/errors/README.md +1 -1
- package/src/configurator/auth/schema.ts +1 -1
- package/src/configurator/index.ts +1 -1
- package/src/configurator/private/schema.ts +1 -1
- package/src/layouts/AppLayout/BaseApp.tsx +25 -43
- package/src/layouts/AppLayout/README.md +1 -2
- package/src/layouts/AuthLayout/AuthLayout.tsx +6 -6
- package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +2 -1
- package/src/layouts/AuthLayout/context.tsx +0 -1
- package/src/layouts/AuthLayout/types.ts +6 -3
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +2 -7
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +1 -1
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +3 -8
- package/src/layouts/PrivateLayout/components/SidebarFeatured.tsx +10 -6
- package/src/layouts/PublicLayout/components/UserMenu.tsx +0 -1
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +1 -1
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +5 -1
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +6 -3
- package/src/layouts/types/index.ts +0 -1
- package/src/layouts/types/layout.types.ts +1 -4
- package/src/layouts/types/providers.types.ts +1 -14
- package/src/snippets/Breadcrumbs.tsx +3 -3
- package/src/testing/MockAuthFormProvider.tsx +0 -1
package/README.md
CHANGED
|
@@ -193,7 +193,7 @@ send analytics directly from a layout.
|
|
|
193
193
|
| `@djangocfg/layouts/layouts` | Layout components. |
|
|
194
194
|
| `@djangocfg/layouts/components` | Misc + `ErrorLayout`, `RedirectPage`. |
|
|
195
195
|
| `@djangocfg/layouts/pages/legal` | Legal page templates. |
|
|
196
|
-
| `@djangocfg/layouts/configurator` | JSON Schemas for `<JsonSchemaForm>`-driven configurator UIs (PrivateLayout for now). Pair with `@djangocfg/
|
|
196
|
+
| `@djangocfg/layouts/configurator` | JSON Schemas for `<JsonSchemaForm>`-driven configurator UIs (PrivateLayout for now). Pair with `@djangocfg/widget-forms`. |
|
|
197
197
|
| `@djangocfg/layouts/styles` | Base CSS. |
|
|
198
198
|
|
|
199
199
|
Extensions: `@djangocfg/ext-newsletter`, `ext-knowbase`, `ext-leads`, `ext-payments`, `ext-support`, …
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.542",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"files": [
|
|
82
82
|
"dist",
|
|
83
83
|
"src",
|
|
84
|
+
"!src/**/*.stories.tsx",
|
|
84
85
|
"README.md",
|
|
85
86
|
"LICENSE"
|
|
86
87
|
],
|
|
@@ -89,12 +90,11 @@
|
|
|
89
90
|
"check": "tsc --noEmit"
|
|
90
91
|
},
|
|
91
92
|
"peerDependencies": {
|
|
92
|
-
"@djangocfg/analytics": "^2.1.
|
|
93
|
-
"@djangocfg/api": "^2.1.
|
|
94
|
-
"@djangocfg/
|
|
95
|
-
"@djangocfg/
|
|
96
|
-
"@djangocfg/
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.541",
|
|
93
|
+
"@djangocfg/analytics": "^2.1.542",
|
|
94
|
+
"@djangocfg/api": "^2.1.542",
|
|
95
|
+
"@djangocfg/devtools": "^2.1.542",
|
|
96
|
+
"@djangocfg/i18n": "^2.1.542",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.542",
|
|
98
98
|
"@hookform/resolvers": "^5.2.2",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -102,8 +102,8 @@
|
|
|
102
102
|
"next": "^16.2.2",
|
|
103
103
|
"next-intl": "^4.9.1",
|
|
104
104
|
"p-retry": "^7.0.0",
|
|
105
|
-
"react": "^19.
|
|
106
|
-
"react-dom": "^19.
|
|
105
|
+
"react": "^19.0.0",
|
|
106
|
+
"react-dom": "^19.0.0",
|
|
107
107
|
"react-hook-form": "^7.69.0",
|
|
108
108
|
"sonner": "2.0.7",
|
|
109
109
|
"swr": "^2.3.7",
|
|
@@ -124,19 +124,19 @@
|
|
|
124
124
|
"uuid": "^11.1.1"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
|
-
"@djangocfg/analytics": "^2.1.
|
|
128
|
-
"@djangocfg/api": "^2.1.
|
|
129
|
-
"@djangocfg/
|
|
130
|
-
"@djangocfg/
|
|
131
|
-
"@djangocfg/
|
|
132
|
-
"@djangocfg/
|
|
133
|
-
"@
|
|
134
|
-
"@djangocfg/ui-tools": "^2.1.541",
|
|
127
|
+
"@djangocfg/analytics": "^2.1.542",
|
|
128
|
+
"@djangocfg/api": "^2.1.542",
|
|
129
|
+
"@djangocfg/devtools": "^2.1.542",
|
|
130
|
+
"@djangocfg/i18n": "^2.1.542",
|
|
131
|
+
"@djangocfg/typescript-config": "^2.1.542",
|
|
132
|
+
"@djangocfg/ui-core": "^2.1.542",
|
|
133
|
+
"@storybook/react-vite": "^10.5.0",
|
|
135
134
|
"@types/node": "^25.9.5",
|
|
136
135
|
"@types/react": "19.2.15",
|
|
137
136
|
"@types/react-dom": "19.2.3",
|
|
138
137
|
"eslint": "^9.39.5",
|
|
139
138
|
"next-intl": "^4.13.2",
|
|
139
|
+
"storybook": "^10.5.0",
|
|
140
140
|
"typescript": "^5.9.3",
|
|
141
141
|
"zustand": "^5.0.14"
|
|
142
142
|
},
|
|
@@ -14,21 +14,20 @@ import { useCopy } from '@djangocfg/ui-core/hooks';
|
|
|
14
14
|
|
|
15
15
|
import { generateCurlFromError } from '../utils/curl-generator';
|
|
16
16
|
import {
|
|
17
|
-
formatCentrifugoErrorForClipboard,
|
|
18
17
|
formatCORSErrorForClipboard,
|
|
19
18
|
formatNetworkErrorForClipboard,
|
|
20
19
|
formatValidationErrorForClipboard
|
|
21
20
|
} from '../utils/formatters';
|
|
22
21
|
|
|
23
|
-
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail
|
|
22
|
+
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail } from '../types';
|
|
24
23
|
export interface ErrorButtonsProps {
|
|
25
|
-
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail
|
|
24
|
+
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Format error for clipboard based on type
|
|
30
29
|
*/
|
|
31
|
-
function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail
|
|
30
|
+
function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail): string {
|
|
32
31
|
switch (detail.type) {
|
|
33
32
|
case 'validation':
|
|
34
33
|
return formatValidationErrorForClipboard(detail);
|
|
@@ -36,8 +35,6 @@ function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail
|
|
|
36
35
|
return formatCORSErrorForClipboard(detail);
|
|
37
36
|
case 'network':
|
|
38
37
|
return formatNetworkErrorForClipboard(detail);
|
|
39
|
-
case 'centrifugo':
|
|
40
|
-
return formatCentrifugoErrorForClipboard(detail);
|
|
41
38
|
default:
|
|
42
39
|
return JSON.stringify(detail, null, 2);
|
|
43
40
|
}
|
|
@@ -46,7 +43,7 @@ function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail
|
|
|
46
43
|
/**
|
|
47
44
|
* Check if error supports cURL generation
|
|
48
45
|
*/
|
|
49
|
-
function supportsCurl(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail
|
|
46
|
+
function supportsCurl(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail): boolean {
|
|
50
47
|
return detail.type === 'validation';
|
|
51
48
|
}
|
|
52
49
|
|
|
@@ -17,12 +17,10 @@ import type {
|
|
|
17
17
|
ValidationErrorDetail,
|
|
18
18
|
CORSErrorDetail,
|
|
19
19
|
NetworkErrorDetail,
|
|
20
|
-
CentrifugoErrorDetail,
|
|
21
20
|
RuntimeErrorDetail,
|
|
22
21
|
ValidationErrorConfig,
|
|
23
22
|
CORSErrorConfig,
|
|
24
23
|
NetworkErrorConfig,
|
|
25
|
-
CentrifugoErrorConfig,
|
|
26
24
|
RuntimeErrorConfig,
|
|
27
25
|
} from '../types';
|
|
28
26
|
/**
|
|
@@ -130,7 +128,7 @@ function buildNetworkDescription(
|
|
|
130
128
|
<div className="opacity-90">{detail.error}</div>
|
|
131
129
|
|
|
132
130
|
{dpopHint && (
|
|
133
|
-
<div className="rounded-md bg-
|
|
131
|
+
<div className="rounded-md bg-warning/10 px-2 py-1 text-xs text-warning-foreground">
|
|
134
132
|
{dpopHint}
|
|
135
133
|
</div>
|
|
136
134
|
)}
|
|
@@ -140,40 +138,6 @@ function buildNetworkDescription(
|
|
|
140
138
|
);
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
/**
|
|
144
|
-
* Build centrifugo error description
|
|
145
|
-
*/
|
|
146
|
-
function buildCentrifugoDescription(
|
|
147
|
-
detail: CentrifugoErrorDetail,
|
|
148
|
-
config: Required<CentrifugoErrorConfig>
|
|
149
|
-
): React.ReactNode {
|
|
150
|
-
const parts: string[] = [];
|
|
151
|
-
|
|
152
|
-
// Add method info
|
|
153
|
-
if (config.showMethod) {
|
|
154
|
-
parts.push(`RPC: ${detail.method}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Add error code
|
|
158
|
-
if (config.showCode && detail.code !== undefined) {
|
|
159
|
-
parts.push(`Code: ${detail.code}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
<div className="flex flex-col gap-2 text-sm">
|
|
164
|
-
{parts.length > 0 && (
|
|
165
|
-
<div className="font-mono text-xs opacity-90">
|
|
166
|
-
{parts.join(' • ')}
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
169
|
-
|
|
170
|
-
<div className="opacity-90">{detail.error}</div>
|
|
171
|
-
|
|
172
|
-
<ErrorButtons detail={detail} />
|
|
173
|
-
</div>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
141
|
/**
|
|
178
142
|
* Build runtime error description
|
|
179
143
|
*/
|
|
@@ -198,8 +162,8 @@ function buildRuntimeDescription(
|
|
|
198
162
|
* Create toast options for any error type
|
|
199
163
|
*/
|
|
200
164
|
export function createErrorToast(
|
|
201
|
-
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail |
|
|
202
|
-
config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig |
|
|
165
|
+
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | RuntimeErrorDetail,
|
|
166
|
+
config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig | RuntimeErrorConfig>
|
|
203
167
|
) {
|
|
204
168
|
let description: React.ReactNode;
|
|
205
169
|
|
|
@@ -208,8 +172,6 @@ export function createErrorToast(
|
|
|
208
172
|
description = buildValidationDescription(detail, config as Required<ValidationErrorConfig>);
|
|
209
173
|
} else if (detail.type === 'cors') {
|
|
210
174
|
description = buildCORSDescription(detail, config as Required<CORSErrorConfig>);
|
|
211
|
-
} else if (detail.type === 'centrifugo') {
|
|
212
|
-
description = buildCentrifugoDescription(detail, config as Required<CentrifugoErrorConfig>);
|
|
213
175
|
} else if (detail.type === 'runtime') {
|
|
214
176
|
description = buildRuntimeDescription(detail, config as Required<RuntimeErrorConfig>);
|
|
215
177
|
} else {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error Tracking - Unified error tracking for all error types
|
|
3
3
|
*
|
|
4
|
-
* Single provider and hook for validation, CORS, network,
|
|
4
|
+
* Single provider and hook for validation, CORS, network, and runtime errors
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Main provider and hook
|
|
@@ -21,14 +21,12 @@ export type {
|
|
|
21
21
|
ValidationErrorDetail,
|
|
22
22
|
CORSErrorDetail,
|
|
23
23
|
NetworkErrorDetail,
|
|
24
|
-
CentrifugoErrorDetail,
|
|
25
24
|
RuntimeErrorDetail,
|
|
26
25
|
StoredError,
|
|
27
26
|
ErrorTrackingConfig,
|
|
28
27
|
ValidationErrorConfig,
|
|
29
28
|
CORSErrorConfig,
|
|
30
29
|
NetworkErrorConfig,
|
|
31
|
-
CentrifugoErrorConfig,
|
|
32
30
|
RuntimeErrorConfig,
|
|
33
31
|
ErrorTrackingContextValue,
|
|
34
32
|
TrackedErrorType,
|
|
@@ -47,7 +45,6 @@ export {
|
|
|
47
45
|
formatValidationErrorForClipboard,
|
|
48
46
|
formatCORSErrorForClipboard,
|
|
49
47
|
formatNetworkErrorForClipboard,
|
|
50
|
-
formatCentrifugoErrorForClipboard,
|
|
51
48
|
formatRuntimeErrorForClipboard,
|
|
52
49
|
formatErrorTitle,
|
|
53
50
|
extractDomain,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
'use client';
|
|
28
28
|
|
|
29
29
|
import React, {
|
|
30
|
-
createContext, ReactNode, useCallback, useContext, useEffect, useState
|
|
30
|
+
createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState
|
|
31
31
|
} from 'react';
|
|
32
32
|
|
|
33
33
|
import { toast } from '@djangocfg/ui-core/hooks';
|
|
@@ -36,7 +36,6 @@ import { useDebugMode, isProduction } from '@djangocfg/devtools';
|
|
|
36
36
|
import { createErrorToast } from '../components/ErrorToast';
|
|
37
37
|
import { generateShortErrorId, getProductionToast } from '../utils/formatters';
|
|
38
38
|
import {
|
|
39
|
-
DEFAULT_CENTRIFUGO_CONFIG,
|
|
40
39
|
DEFAULT_CORS_CONFIG,
|
|
41
40
|
DEFAULT_NETWORK_CONFIG,
|
|
42
41
|
DEFAULT_RUNTIME_CONFIG,
|
|
@@ -50,12 +49,10 @@ import type {
|
|
|
50
49
|
ValidationErrorConfig,
|
|
51
50
|
CORSErrorConfig,
|
|
52
51
|
NetworkErrorConfig,
|
|
53
|
-
CentrifugoErrorConfig,
|
|
54
52
|
RuntimeErrorConfig,
|
|
55
53
|
ValidationErrorDetail,
|
|
56
54
|
CORSErrorDetail,
|
|
57
55
|
NetworkErrorDetail,
|
|
58
|
-
CentrifugoErrorDetail,
|
|
59
56
|
RuntimeErrorDetail,
|
|
60
57
|
ErrorTrackingContextValue,
|
|
61
58
|
TrackedErrorType,
|
|
@@ -82,8 +79,6 @@ function getErrorDedupeKey(detail: ErrorDetail): string {
|
|
|
82
79
|
return `cors:${detail.url}`;
|
|
83
80
|
case 'network':
|
|
84
81
|
return `network:${detail.url}:${detail.statusCode || 'unknown'}`;
|
|
85
|
-
case 'centrifugo':
|
|
86
|
-
return `centrifugo:${detail.method}:${detail.code || 'unknown'}`;
|
|
87
82
|
case 'runtime':
|
|
88
83
|
return `runtime:${detail.source}:${detail.message.slice(0, 50)}`;
|
|
89
84
|
default:
|
|
@@ -169,7 +164,6 @@ export interface ErrorTrackingProviderProps {
|
|
|
169
164
|
validation?: Partial<ValidationErrorConfig>;
|
|
170
165
|
cors?: Partial<CORSErrorConfig>;
|
|
171
166
|
network?: Partial<NetworkErrorConfig>;
|
|
172
|
-
centrifugo?: Partial<CentrifugoErrorConfig>;
|
|
173
167
|
runtime?: Partial<RuntimeErrorConfig>;
|
|
174
168
|
onError?: (error: ErrorDetail) => boolean | void;
|
|
175
169
|
/** Called with the raw ErrorDetail for every tracked error. Wire to devtools.capture() (via errorDetailToDevtoolsEvent) to forward errors to the backend. */
|
|
@@ -186,7 +180,6 @@ export function ErrorTrackingProvider({
|
|
|
186
180
|
validation: userValidationConfig,
|
|
187
181
|
cors: userCorsConfig,
|
|
188
182
|
network: userNetworkConfig,
|
|
189
|
-
centrifugo: userCentrifugoConfig,
|
|
190
183
|
runtime: userRuntimeConfig,
|
|
191
184
|
onError,
|
|
192
185
|
onMonitorCapture,
|
|
@@ -194,31 +187,29 @@ export function ErrorTrackingProvider({
|
|
|
194
187
|
const [errors, setErrors] = useState<StoredError[]>([]);
|
|
195
188
|
const isDebugMode = useDebugMode();
|
|
196
189
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
...userCorsConfig,
|
|
206
|
-
};
|
|
190
|
+
// Merged configs must be referentially stable: they feed the listener effect
|
|
191
|
+
// below, and a fresh object each render would detach and re-attach every
|
|
192
|
+
// window listener on every render — including the re-render each tracked
|
|
193
|
+
// error causes, dropping any error dispatched inside that gap.
|
|
194
|
+
const validationConfig: Required<ValidationErrorConfig> = useMemo(
|
|
195
|
+
() => ({ ...DEFAULT_VALIDATION_CONFIG, ...userValidationConfig }),
|
|
196
|
+
[userValidationConfig]
|
|
197
|
+
);
|
|
207
198
|
|
|
208
|
-
const
|
|
209
|
-
...
|
|
210
|
-
|
|
211
|
-
|
|
199
|
+
const corsConfig: Required<CORSErrorConfig> = useMemo(
|
|
200
|
+
() => ({ ...DEFAULT_CORS_CONFIG, ...userCorsConfig }),
|
|
201
|
+
[userCorsConfig]
|
|
202
|
+
);
|
|
212
203
|
|
|
213
|
-
const
|
|
214
|
-
...
|
|
215
|
-
|
|
216
|
-
|
|
204
|
+
const networkConfig: Required<NetworkErrorConfig> = useMemo(
|
|
205
|
+
() => ({ ...DEFAULT_NETWORK_CONFIG, ...userNetworkConfig }),
|
|
206
|
+
[userNetworkConfig]
|
|
207
|
+
);
|
|
217
208
|
|
|
218
|
-
const runtimeConfig: Required<RuntimeErrorConfig> =
|
|
219
|
-
...DEFAULT_RUNTIME_CONFIG,
|
|
220
|
-
|
|
221
|
-
|
|
209
|
+
const runtimeConfig: Required<RuntimeErrorConfig> = useMemo(
|
|
210
|
+
() => ({ ...DEFAULT_RUNTIME_CONFIG, ...userRuntimeConfig }),
|
|
211
|
+
[userRuntimeConfig]
|
|
212
|
+
);
|
|
222
213
|
|
|
223
214
|
/**
|
|
224
215
|
* Clear all errors
|
|
@@ -245,7 +236,7 @@ export function ErrorTrackingProvider({
|
|
|
245
236
|
* Handle any error event
|
|
246
237
|
*/
|
|
247
238
|
const handleError = useCallback(
|
|
248
|
-
(detail: ErrorDetail, config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig |
|
|
239
|
+
(detail: ErrorDetail, config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig | RuntimeErrorConfig>) => {
|
|
249
240
|
// Check for duplicate errors (within deduplication window)
|
|
250
241
|
const isUnique = shouldShowError(detail);
|
|
251
242
|
|
|
@@ -287,9 +278,19 @@ export function ErrorTrackingProvider({
|
|
|
287
278
|
}
|
|
288
279
|
}
|
|
289
280
|
},
|
|
290
|
-
|
|
281
|
+
// `isProduction` is a module constant, not reactive state.
|
|
282
|
+
[onError, onMonitorCapture, isDebugMode]
|
|
291
283
|
);
|
|
292
284
|
|
|
285
|
+
// The listener effect must not re-run when the handler changes identity: hosts
|
|
286
|
+
// pass `onError`/`onMonitorCapture` as inline arrows, so `handleError` is new
|
|
287
|
+
// on every render. Listeners read the current handler through this ref and
|
|
288
|
+
// stay attached for the provider's whole lifetime.
|
|
289
|
+
const handleErrorRef = useRef(handleError);
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
handleErrorRef.current = handleError;
|
|
292
|
+
}, [handleError]);
|
|
293
|
+
|
|
293
294
|
/**
|
|
294
295
|
* Setup event listeners
|
|
295
296
|
*/
|
|
@@ -318,7 +319,7 @@ export function ErrorTrackingProvider({
|
|
|
318
319
|
error: error as ValidationErrorDetail['error'],
|
|
319
320
|
response: raw.response ?? raw.data,
|
|
320
321
|
};
|
|
321
|
-
|
|
322
|
+
handleErrorRef.current(detail, validationConfig);
|
|
322
323
|
};
|
|
323
324
|
window.addEventListener(ERROR_EVENTS.VALIDATION, handler);
|
|
324
325
|
handlers.push({ event: ERROR_EVENTS.VALIDATION, handler });
|
|
@@ -332,32 +333,12 @@ export function ErrorTrackingProvider({
|
|
|
332
333
|
...event.detail,
|
|
333
334
|
type: 'network' as const,
|
|
334
335
|
};
|
|
335
|
-
|
|
336
|
+
handleErrorRef.current(detail, networkConfig);
|
|
336
337
|
};
|
|
337
338
|
window.addEventListener(ERROR_EVENTS.NETWORK, handler);
|
|
338
339
|
handlers.push({ event: ERROR_EVENTS.NETWORK, handler });
|
|
339
340
|
}
|
|
340
341
|
|
|
341
|
-
// Centrifugo errors (unified event with type discriminator)
|
|
342
|
-
if (centrifugoConfig.enabled) {
|
|
343
|
-
const handler = (event: Event) => {
|
|
344
|
-
if (!(event instanceof CustomEvent)) return;
|
|
345
|
-
// Filter: only handle 'error' type from unified centrifugo event
|
|
346
|
-
if (event.detail?.type !== 'error') return;
|
|
347
|
-
const detail: CentrifugoErrorDetail = {
|
|
348
|
-
type: 'centrifugo' as const,
|
|
349
|
-
method: event.detail.data?.method || 'unknown',
|
|
350
|
-
error: event.detail.data?.error || 'Unknown error',
|
|
351
|
-
code: event.detail.data?.code,
|
|
352
|
-
data: event.detail.data?.data,
|
|
353
|
-
timestamp: event.detail.timestamp || new Date(),
|
|
354
|
-
};
|
|
355
|
-
handleError(detail, centrifugoConfig);
|
|
356
|
-
};
|
|
357
|
-
window.addEventListener(ERROR_EVENTS.CENTRIFUGO, handler);
|
|
358
|
-
handlers.push({ event: ERROR_EVENTS.CENTRIFUGO, handler });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
342
|
// Runtime errors (generic JS errors from anywhere in the app)
|
|
362
343
|
if (runtimeConfig.enabled) {
|
|
363
344
|
const handler = (event: Event) => {
|
|
@@ -370,7 +351,7 @@ export function ErrorTrackingProvider({
|
|
|
370
351
|
context: event.detail.context,
|
|
371
352
|
timestamp: event.detail.timestamp || new Date(),
|
|
372
353
|
};
|
|
373
|
-
|
|
354
|
+
handleErrorRef.current(detail, runtimeConfig);
|
|
374
355
|
};
|
|
375
356
|
window.addEventListener(ERROR_EVENTS.RUNTIME, handler);
|
|
376
357
|
handlers.push({ event: ERROR_EVENTS.RUNTIME, handler });
|
|
@@ -382,13 +363,15 @@ export function ErrorTrackingProvider({
|
|
|
382
363
|
window.removeEventListener(event, handler);
|
|
383
364
|
});
|
|
384
365
|
};
|
|
385
|
-
|
|
366
|
+
// No `cors` listener: nothing dispatches `cors-error`. A blocked request
|
|
367
|
+
// surfaces as a network error carrying `possibly_cors`, so `corsConfig`
|
|
368
|
+
// configures presentation only and is not a dependency here.
|
|
369
|
+
}, [validationConfig, networkConfig, runtimeConfig]);
|
|
386
370
|
|
|
387
371
|
// Filter errors by type
|
|
388
372
|
const validationErrors = errors.filter((e) => e.type === 'validation') as StoredError<ValidationErrorDetail>[];
|
|
389
373
|
const corsErrors = errors.filter((e) => e.type === 'cors') as StoredError<CORSErrorDetail>[];
|
|
390
374
|
const networkErrors = errors.filter((e) => e.type === 'network') as StoredError<NetworkErrorDetail>[];
|
|
391
|
-
const centrifugoErrors = errors.filter((e) => e.type === 'centrifugo') as StoredError<CentrifugoErrorDetail>[];
|
|
392
375
|
const runtimeErrors = errors.filter((e) => e.type === 'runtime') as StoredError<RuntimeErrorDetail>[];
|
|
393
376
|
|
|
394
377
|
const value: ErrorTrackingContextValue = {
|
|
@@ -396,7 +379,6 @@ export function ErrorTrackingProvider({
|
|
|
396
379
|
validationErrors,
|
|
397
380
|
corsErrors,
|
|
398
381
|
networkErrors,
|
|
399
|
-
centrifugoErrors,
|
|
400
382
|
runtimeErrors,
|
|
401
383
|
clearErrors,
|
|
402
384
|
clearErrorsByType,
|
|
@@ -407,7 +389,6 @@ export function ErrorTrackingProvider({
|
|
|
407
389
|
validation: validationConfig,
|
|
408
390
|
cors: corsConfig,
|
|
409
391
|
network: networkConfig,
|
|
410
|
-
centrifugo: centrifugoConfig,
|
|
411
392
|
runtime: runtimeConfig,
|
|
412
393
|
},
|
|
413
394
|
};
|
|
@@ -28,7 +28,7 @@ export interface ValidationErrorDetail extends BaseErrorDetail {
|
|
|
28
28
|
/** Zod validation error */
|
|
29
29
|
error: ZodError;
|
|
30
30
|
/** Raw response data that failed validation */
|
|
31
|
-
response:
|
|
31
|
+
response: unknown;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -65,21 +65,6 @@ export interface NetworkErrorDetail extends BaseErrorDetail {
|
|
|
65
65
|
possibly_cors?: boolean;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
* Centrifugo error detail (from centrifugo-error event)
|
|
70
|
-
*/
|
|
71
|
-
export interface CentrifugoErrorDetail extends BaseErrorDetail {
|
|
72
|
-
type: 'centrifugo';
|
|
73
|
-
/** RPC method that failed */
|
|
74
|
-
method: string;
|
|
75
|
-
/** Error message */
|
|
76
|
-
error: string;
|
|
77
|
-
/** Error code from Centrifugo */
|
|
78
|
-
code?: number;
|
|
79
|
-
/** Additional data sent with the request */
|
|
80
|
-
data?: any;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
68
|
/**
|
|
84
69
|
* Every error category the tracker stores.
|
|
85
70
|
*
|
|
@@ -94,7 +79,6 @@ export type TrackedErrorType =
|
|
|
94
79
|
| 'validation'
|
|
95
80
|
| 'cors'
|
|
96
81
|
| 'network'
|
|
97
|
-
| 'centrifugo'
|
|
98
82
|
| 'runtime';
|
|
99
83
|
|
|
100
84
|
/**
|
|
@@ -116,7 +100,7 @@ export interface RuntimeErrorDetail extends BaseErrorDetail {
|
|
|
116
100
|
/**
|
|
117
101
|
* Union type of all error details
|
|
118
102
|
*/
|
|
119
|
-
export type ErrorDetail = ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail |
|
|
103
|
+
export type ErrorDetail = ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | RuntimeErrorDetail;
|
|
120
104
|
|
|
121
105
|
/**
|
|
122
106
|
* Stored error with unique ID
|
|
@@ -224,23 +208,6 @@ export interface NetworkErrorConfig extends ErrorTypeConfig {
|
|
|
224
208
|
showStatusCode?: boolean;
|
|
225
209
|
}
|
|
226
210
|
|
|
227
|
-
/**
|
|
228
|
-
* Centrifugo error specific config
|
|
229
|
-
*/
|
|
230
|
-
export interface CentrifugoErrorConfig extends ErrorTypeConfig {
|
|
231
|
-
/**
|
|
232
|
-
* Show RPC method in toast
|
|
233
|
-
* @default true
|
|
234
|
-
*/
|
|
235
|
-
showMethod?: boolean;
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Show error code in toast
|
|
239
|
-
* @default true
|
|
240
|
-
*/
|
|
241
|
-
showCode?: boolean;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
211
|
/**
|
|
245
212
|
* Runtime error specific config
|
|
246
213
|
*/
|
|
@@ -271,11 +238,6 @@ export interface ErrorTrackingConfig {
|
|
|
271
238
|
*/
|
|
272
239
|
network?: NetworkErrorConfig;
|
|
273
240
|
|
|
274
|
-
/**
|
|
275
|
-
* Centrifugo error tracking configuration
|
|
276
|
-
*/
|
|
277
|
-
centrifugo?: CentrifugoErrorConfig;
|
|
278
|
-
|
|
279
241
|
/**
|
|
280
242
|
* Runtime error tracking configuration
|
|
281
243
|
*/
|
|
@@ -304,9 +266,6 @@ export interface ErrorTrackingContextValue {
|
|
|
304
266
|
/** Network errors only */
|
|
305
267
|
networkErrors: StoredError<NetworkErrorDetail>[];
|
|
306
268
|
|
|
307
|
-
/** Centrifugo errors only */
|
|
308
|
-
centrifugoErrors: StoredError<CentrifugoErrorDetail>[];
|
|
309
|
-
|
|
310
269
|
/** Runtime errors only */
|
|
311
270
|
runtimeErrors: StoredError<RuntimeErrorDetail>[];
|
|
312
271
|
|
|
@@ -330,7 +289,6 @@ export interface ErrorTrackingContextValue {
|
|
|
330
289
|
validation: Required<ValidationErrorConfig>;
|
|
331
290
|
cors: Required<CORSErrorConfig>;
|
|
332
291
|
network: Required<NetworkErrorConfig>;
|
|
333
|
-
centrifugo: Required<CentrifugoErrorConfig>;
|
|
334
292
|
runtime: Required<RuntimeErrorConfig>;
|
|
335
293
|
};
|
|
336
294
|
}
|
|
@@ -342,8 +300,6 @@ export const ERROR_EVENTS = {
|
|
|
342
300
|
VALIDATION: 'zod-validation-error',
|
|
343
301
|
CORS: 'cors-error',
|
|
344
302
|
NETWORK: 'network-error',
|
|
345
|
-
/** Unified Centrifugo event - filter by detail.type === 'error' */
|
|
346
|
-
CENTRIFUGO: 'centrifugo',
|
|
347
303
|
/** Runtime/generic errors (Tour, components, etc.) */
|
|
348
304
|
RUNTIME: 'runtime-error',
|
|
349
305
|
} as const;
|
|
@@ -381,13 +337,6 @@ export const DEFAULT_NETWORK_CONFIG: Required<NetworkErrorConfig> = {
|
|
|
381
337
|
showStatusCode: true,
|
|
382
338
|
};
|
|
383
339
|
|
|
384
|
-
export const DEFAULT_CENTRIFUGO_CONFIG: Required<CentrifugoErrorConfig> = {
|
|
385
|
-
...DEFAULT_ERROR_CONFIG,
|
|
386
|
-
duration: 0, // Don't auto-dismiss centrifugo errors
|
|
387
|
-
showMethod: true,
|
|
388
|
-
showCode: true,
|
|
389
|
-
};
|
|
390
|
-
|
|
391
340
|
export const DEFAULT_RUNTIME_CONFIG: Required<RuntimeErrorConfig> = {
|
|
392
341
|
...DEFAULT_ERROR_CONFIG,
|
|
393
342
|
duration: 8000,
|
|
@@ -13,7 +13,7 @@ export interface CurlOptions {
|
|
|
13
13
|
method: string;
|
|
14
14
|
path: string;
|
|
15
15
|
token?: string;
|
|
16
|
-
body?:
|
|
16
|
+
body?: unknown;
|
|
17
17
|
headers?: Record<string, string>;
|
|
18
18
|
baseUrl?: string;
|
|
19
19
|
queryParams?: Record<string, string>;
|
|
@@ -126,7 +126,7 @@ export function generateCurl(options: CurlOptions): string {
|
|
|
126
126
|
export function generateCurlFromError(detail: {
|
|
127
127
|
method: string;
|
|
128
128
|
path: string;
|
|
129
|
-
response?:
|
|
129
|
+
response?: unknown;
|
|
130
130
|
}): string {
|
|
131
131
|
return generateCurl({
|
|
132
132
|
method: detail.method,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ZodError } from 'zod';
|
|
8
|
-
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail,
|
|
8
|
+
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail, RuntimeErrorDetail, ErrorDetail } from '../types';
|
|
9
9
|
import type { DevtoolsEvent } from '@djangocfg/devtools';
|
|
10
10
|
import { EventType, EventLevel } from '@djangocfg/devtools';
|
|
11
11
|
|
|
@@ -99,22 +99,6 @@ export function formatNetworkErrorForClipboard(detail: NetworkErrorDetail): stri
|
|
|
99
99
|
return JSON.stringify(errorData, null, 2);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/**
|
|
103
|
-
* Format centrifugo error for clipboard
|
|
104
|
-
*/
|
|
105
|
-
export function formatCentrifugoErrorForClipboard(detail: CentrifugoErrorDetail): string {
|
|
106
|
-
const errorData = {
|
|
107
|
-
type: 'centrifugo',
|
|
108
|
-
timestamp: detail.timestamp.toISOString(),
|
|
109
|
-
method: detail.method,
|
|
110
|
-
error: detail.error,
|
|
111
|
-
...(detail.code !== undefined && { code: detail.code }),
|
|
112
|
-
...(detail.data && { data: detail.data }),
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
return JSON.stringify(errorData, null, 2);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
102
|
/**
|
|
119
103
|
* Extract domain from URL
|
|
120
104
|
*/
|
|
@@ -160,8 +144,6 @@ export function errorDetailToDevtoolsEvent(detail: ErrorDetail): DevtoolsEvent {
|
|
|
160
144
|
return { event_type: EventType.NETWORK_ERROR, level: EventLevel.ERROR, message: `CORS: ${detail.method} ${detail.url}`, http_method: detail.method, http_url: detail.url, url }
|
|
161
145
|
case 'network':
|
|
162
146
|
return { event_type: EventType.NETWORK_ERROR, level: detail.statusCode && detail.statusCode >= 500 ? EventLevel.ERROR : EventLevel.WARNING, message: `${detail.method} ${detail.url} → ${detail.statusCode ?? 'ERR'}`, http_method: detail.method, http_url: detail.url, http_status: detail.statusCode, url }
|
|
163
|
-
case 'centrifugo':
|
|
164
|
-
return { event_type: EventType.ERROR, level: EventLevel.ERROR, message: `Centrifugo ${detail.method}: ${detail.error}`, url }
|
|
165
147
|
case 'runtime':
|
|
166
148
|
return { event_type: EventType.JS_ERROR, level: EventLevel.ERROR, message: detail.message, stack_trace: detail.error?.stack, url }
|
|
167
149
|
}
|
|
@@ -170,7 +152,7 @@ export function errorDetailToDevtoolsEvent(detail: ErrorDetail): DevtoolsEvent {
|
|
|
170
152
|
/**
|
|
171
153
|
* Format error title based on type (dev/debug mode — full details)
|
|
172
154
|
*/
|
|
173
|
-
export function formatErrorTitle(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail |
|
|
155
|
+
export function formatErrorTitle(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | RuntimeErrorDetail): string {
|
|
174
156
|
switch (detail.type) {
|
|
175
157
|
case 'validation':
|
|
176
158
|
return `❌ Validation Error in ${detail.operation}`;
|
|
@@ -180,10 +162,6 @@ export function formatErrorTitle(detail: ValidationErrorDetail | CORSErrorDetail
|
|
|
180
162
|
return detail.statusCode
|
|
181
163
|
? `⚠️ Network Error (${detail.statusCode})`
|
|
182
164
|
: '⚠️ Network Error';
|
|
183
|
-
case 'centrifugo':
|
|
184
|
-
return detail.code !== undefined
|
|
185
|
-
? `🔌 Centrifugo Error (${detail.code})`
|
|
186
|
-
: '🔌 Centrifugo Error';
|
|
187
165
|
case 'runtime':
|
|
188
166
|
return `⚠️ ${detail.source} Error`;
|
|
189
167
|
default:
|
|
@@ -242,11 +220,6 @@ export function getProductionToast(detail: ErrorDetail, errorId: string): { titl
|
|
|
242
220
|
description: `Please check your connection and try again. Reference: ${errorId}`,
|
|
243
221
|
};
|
|
244
222
|
}
|
|
245
|
-
case 'centrifugo':
|
|
246
|
-
return {
|
|
247
|
-
title: 'Real-time connection error',
|
|
248
|
-
description: `Live updates may be delayed. Reference: ${errorId}`,
|
|
249
|
-
};
|
|
250
223
|
case 'runtime':
|
|
251
224
|
return {
|
|
252
225
|
title: 'Something went wrong',
|
|
@@ -48,7 +48,7 @@ This is a *user-facing recovery* path. `ErrorsTracker` below is the
|
|
|
48
48
|
the generated API client and runtime, then renders a Sonner toast per error.
|
|
49
49
|
|
|
50
50
|
Tracked error types: `validation` (zod response mismatch), `network` (failed/4xx/5xx
|
|
51
|
-
requests, incl. a CORS heuristic),
|
|
51
|
+
requests, incl. a CORS heuristic), and `runtime` (uncaught JS).
|
|
52
52
|
|
|
53
53
|
```tsx
|
|
54
54
|
import { ErrorTrackingProvider } from '@djangocfg/layouts'; // (or the module path)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - `authSplitSchema` / `authSplitUiSchema` — two-column card with sidebar
|
|
7
7
|
*
|
|
8
8
|
* Pair with `<JsonSchemaForm density="compact" schema={...} uiSchema={...}>`
|
|
9
|
-
* from `@djangocfg/
|
|
9
|
+
* from `@djangocfg/widget-forms` to get a ready-made sidebar configurator.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { CustomJsonSchema7, CustomJsonUiSchema7 } from '@djangocfg/ui-core/lib';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JSON Schema descriptors for `<JsonSchemaForm>`-driven layout configurators.
|
|
3
3
|
*
|
|
4
|
-
* Drop a schema + uiSchema into `@djangocfg/
|
|
4
|
+
* Drop a schema + uiSchema into `@djangocfg/widget-forms`'s `<JsonSchemaForm>` and
|
|
5
5
|
* you get a ready-made sidebar that edits the matching `<Layout>` props live.
|
|
6
6
|
*
|
|
7
7
|
* Public surface today:
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - `header.*` → fields consumed by `HeaderConfig`
|
|
9
9
|
*
|
|
10
10
|
* Pair with `<JsonSchemaForm density="compact" schema={...} uiSchema={...}>`
|
|
11
|
-
* from `@djangocfg/
|
|
11
|
+
* from `@djangocfg/widget-forms` to get a ready-made sidebar configurator. The
|
|
12
12
|
* `defaultPrivateLayoutConfiguratorData` is a safe initial value that already
|
|
13
13
|
* matches the schema (no nullable fields, no missing keys).
|
|
14
14
|
*
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
* - AuthProvider (authentication context)
|
|
16
16
|
* - AuthDialog (global auth prompt dialog)
|
|
17
17
|
* - AnalyticsProvider (Google Analytics, optional)
|
|
18
|
-
* - CentrifugoProvider (WebSocket real-time, optional)
|
|
19
18
|
* - PwaProvider (PWA installation, optional)
|
|
20
19
|
* - ErrorTrackingProvider (error handling)
|
|
21
20
|
* - ErrorBoundary (React error boundary, enabled by default)
|
|
@@ -49,9 +48,7 @@ import { SWRConfig } from 'swr';
|
|
|
49
48
|
|
|
50
49
|
import { Devtools, devtools } from '@djangocfg/devtools';
|
|
51
50
|
import { errorDetailToDevtoolsEvent } from '../../components/errors/ErrorsTracker';
|
|
52
|
-
import { CfgCentrifugo } from '@djangocfg/api';
|
|
53
51
|
import { AuthProvider } from '@djangocfg/api/auth';
|
|
54
|
-
import { CentrifugoProvider } from '@djangocfg/centrifugo';
|
|
55
52
|
import { UiProviders } from '@djangocfg/ui-core/providers';
|
|
56
53
|
import { NextRouterAdapter, NextLinkProvider } from '@djangocfg/ui-core/adapters/nextjs';
|
|
57
54
|
import { NextIntlLinkBridge } from './NextIntlLinkBridge';
|
|
@@ -119,7 +116,7 @@ function AppErrorFallback({
|
|
|
119
116
|
* BaseApp - Core providers wrapper for any React/Next.js app
|
|
120
117
|
*
|
|
121
118
|
* Includes: ThemeProvider, TooltipProvider, SWRConfig, AuthProvider, AnalyticsProvider,
|
|
122
|
-
*
|
|
119
|
+
* ErrorTrackingProvider, ErrorBoundary (optional)
|
|
123
120
|
* Also renders global Toaster and PageProgress components
|
|
124
121
|
*/
|
|
125
122
|
export function BaseApp({
|
|
@@ -128,7 +125,6 @@ export function BaseApp({
|
|
|
128
125
|
theme,
|
|
129
126
|
auth,
|
|
130
127
|
analytics,
|
|
131
|
-
centrifugo,
|
|
132
128
|
errorTracking,
|
|
133
129
|
errorBoundary,
|
|
134
130
|
swr,
|
|
@@ -142,10 +138,6 @@ export function BaseApp({
|
|
|
142
138
|
// ErrorBoundary is enabled by default
|
|
143
139
|
const enableErrorBoundary = errorBoundary?.enabled !== false;
|
|
144
140
|
|
|
145
|
-
// Centrifugo configuration
|
|
146
|
-
const centrifugoUrl = centrifugo?.url || process.env.NEXT_PUBLIC_CENTRIFUGO_URL;
|
|
147
|
-
const centrifugoEnabled = centrifugo?.enabled !== false;
|
|
148
|
-
|
|
149
141
|
// Devtools — capture + ingest + debug panel in one component.
|
|
150
142
|
// Project prop is the default; the devtools prop overrides field by field.
|
|
151
143
|
const devtoolsProps = {
|
|
@@ -164,7 +156,7 @@ export function BaseApp({
|
|
|
164
156
|
* (useNavigate, useQueryState, etc.) to next/navigation so
|
|
165
157
|
* server components, route loaders, and prefetch fire
|
|
166
158
|
* correctly. Placed near the top so every provider below
|
|
167
|
-
* (Auth,
|
|
159
|
+
* (Auth, anything that may navigate) gets it.
|
|
168
160
|
*/}
|
|
169
161
|
<NextRouterAdapter>
|
|
170
162
|
<NextLinkProvider>
|
|
@@ -192,41 +184,31 @@ export function BaseApp({
|
|
|
192
184
|
>
|
|
193
185
|
<AuthProvider config={authEnabled ? authConfig : undefined} enabled={authEnabled}>
|
|
194
186
|
<AnalyticsProvider trackingId={analytics?.googleTrackingId}>
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return result.data.token;
|
|
202
|
-
}}
|
|
187
|
+
<ErrorTrackingProvider
|
|
188
|
+
validation={errorTracking?.validation}
|
|
189
|
+
cors={errorTracking?.cors}
|
|
190
|
+
network={errorTracking?.network}
|
|
191
|
+
onError={errorTracking?.onError}
|
|
192
|
+
onMonitorCapture={(d) => devtools.capture(errorDetailToDevtoolsEvent(d))}
|
|
203
193
|
>
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
</LayoutI18nProvider>
|
|
219
|
-
<NextTopLoader
|
|
220
|
-
color="var(--primary)"
|
|
221
|
-
height={3}
|
|
222
|
-
showSpinner={false}
|
|
223
|
-
shadow="0 0 10px var(--primary), 0 0 5px var(--primary)"
|
|
224
|
-
/>
|
|
194
|
+
<Devtools {...devtoolsProps} />
|
|
195
|
+
<LayoutI18nProvider value={i18n}>
|
|
196
|
+
{i18n?.routing ? (
|
|
197
|
+
<NextIntlLinkBridge routing={i18n.routing}>{children}</NextIntlLinkBridge>
|
|
198
|
+
) : (
|
|
199
|
+
children
|
|
200
|
+
)}
|
|
201
|
+
</LayoutI18nProvider>
|
|
202
|
+
<NextTopLoader
|
|
203
|
+
color="var(--primary)"
|
|
204
|
+
height={3}
|
|
205
|
+
showSpinner={false}
|
|
206
|
+
shadow="0 0 10px var(--primary), 0 0 5px var(--primary)"
|
|
207
|
+
/>
|
|
225
208
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
</CentrifugoProvider>
|
|
209
|
+
{/* Auth Dialog - only when auth is enabled */}
|
|
210
|
+
{authEnabled && <AuthDialog authPath={authConfig?.routes?.auth} />}
|
|
211
|
+
</ErrorTrackingProvider>
|
|
230
212
|
</AnalyticsProvider>
|
|
231
213
|
</AuthProvider>
|
|
232
214
|
</SWRConfig>
|
|
@@ -25,7 +25,7 @@ export default function RootLayout({ children }) {
|
|
|
25
25
|
theme={{ defaultTheme: 'system', storageKey: 'myapp-theme' }}
|
|
26
26
|
auth={{ apiUrl: process.env.NEXT_PUBLIC_API_URL,
|
|
27
27
|
routes: { auth: '/auth', defaultCallback: '/private' } }}
|
|
28
|
-
// analytics /
|
|
28
|
+
// analytics / monitor / errorTracking / debug — all optional
|
|
29
29
|
>
|
|
30
30
|
{children}
|
|
31
31
|
</BaseApp>
|
|
@@ -78,7 +78,6 @@ interface BaseAppProps {
|
|
|
78
78
|
theme?: ThemeConfig; // defaultTheme, storageKey, style preset
|
|
79
79
|
auth?: AuthConfig | false; // false = no AuthProvider (fully public app)
|
|
80
80
|
analytics?: AnalyticsConfig; // Google Analytics
|
|
81
|
-
centrifugo?: CentrifugoConfig;
|
|
82
81
|
errorTracking?: ErrorTrackingConfig;
|
|
83
82
|
errorBoundary?: ErrorBoundaryConfig; // enabled by default
|
|
84
83
|
swr?: SWRConfigOptions;
|
|
@@ -104,7 +104,7 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
|
104
104
|
* where a person stopped in the layout, and quietly becomes a no-op when an
|
|
105
105
|
* app intentionally does not mount AnalyticsProvider.
|
|
106
106
|
*/
|
|
107
|
-
const AuthFunnelTracker: React.FC = memo(()
|
|
107
|
+
const AuthFunnelTracker: React.FC = memo(function AuthFunnelTracker() {
|
|
108
108
|
const { step } = useAuthFormContext();
|
|
109
109
|
const { event, isEnabled } = useAnalytics();
|
|
110
110
|
const lastStep = React.useRef<string | null>(null);
|
|
@@ -119,13 +119,13 @@ const AuthFunnelTracker: React.FC = memo(() => {
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
/** Renders custom children only on the identifier step — hides them on otp / 2fa / etc. */
|
|
122
|
-
const AuthHeaderSlot: React.FC<{ children?: React.ReactNode }> = memo(({ children })
|
|
122
|
+
const AuthHeaderSlot: React.FC<{ children?: React.ReactNode }> = memo(function AuthHeaderSlot({ children }) {
|
|
123
123
|
const { step } = useAuthFormContext();
|
|
124
124
|
if (!children || step !== 'identifier') return null;
|
|
125
125
|
return <>{children}</>;
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
const AuthContent: React.FC = memo(()
|
|
128
|
+
const AuthContent: React.FC = memo(function AuthContent() {
|
|
129
129
|
const { step } = useAuthFormContext();
|
|
130
130
|
|
|
131
131
|
switch (step) {
|
|
@@ -145,7 +145,7 @@ const AuthContent: React.FC = memo(() => {
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
const AuthSuccessOverlay: React.FC = memo(()
|
|
148
|
+
const AuthSuccessOverlay: React.FC = memo(function AuthSuccessOverlay() {
|
|
149
149
|
const { step, logo, redirectUrl } = useAuthFormContext();
|
|
150
150
|
|
|
151
151
|
if (step !== 'success') {
|
|
@@ -162,11 +162,11 @@ interface AuthSuccessInlineProps {
|
|
|
162
162
|
redirectDelay?: number;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(({
|
|
165
|
+
const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(function AuthSuccess({
|
|
166
166
|
logo,
|
|
167
167
|
redirectUrl,
|
|
168
168
|
redirectDelay = AUTH.REDIRECT_DELAY,
|
|
169
|
-
})
|
|
169
|
+
}) {
|
|
170
170
|
const router = useCfgRouter();
|
|
171
171
|
const { routes } = useAuth();
|
|
172
172
|
const t = useAppT();
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
import { useAuthFormContext } from '../../context';
|
|
13
13
|
|
|
14
14
|
export interface OAuthCallbackProps {
|
|
15
|
-
|
|
15
|
+
/** `user` is the provider callback's user payload, untyped upstream — narrow it before reading fields. */
|
|
16
|
+
onSuccess?: (user: unknown, isNewUser: boolean, provider: string) => void;
|
|
16
17
|
onError?: (error: string) => void;
|
|
17
18
|
redirectUrl?: string;
|
|
18
19
|
}
|
|
@@ -20,7 +20,7 @@ export type {
|
|
|
20
20
|
|
|
21
21
|
import type { AuthFormReturn } from '@djangocfg/api/auth';
|
|
22
22
|
|
|
23
|
-
import type { AuthBackgroundConfig, AuthShellVariant
|
|
23
|
+
import type { AuthBackgroundConfig, AuthShellVariant } from './shells';
|
|
24
24
|
|
|
25
25
|
// Re-export shell types so consumers can import everything from ./types
|
|
26
26
|
export type { AuthBackgroundConfig, AuthShellVariant, AuthShellContextValue } from './shells';
|
|
@@ -114,8 +114,11 @@ export interface AuthLayoutProps extends AuthLayoutConfig {
|
|
|
114
114
|
onIdentifierSuccess?: (identifier: string) => void;
|
|
115
115
|
/** Callback when OTP verification succeeds */
|
|
116
116
|
onOTPSuccess?: () => void;
|
|
117
|
-
/**
|
|
118
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Callback when OAuth succeeds. `user` is the provider callback's user
|
|
119
|
+
* payload, untyped upstream — narrow it before reading fields.
|
|
120
|
+
*/
|
|
121
|
+
onOAuthSuccess?: (user: unknown, isNewUser: boolean, provider: string) => void;
|
|
119
122
|
/** Callback on any error */
|
|
120
123
|
onError?: (message: string) => void;
|
|
121
124
|
}
|
|
@@ -8,22 +8,17 @@
|
|
|
8
8
|
|
|
9
9
|
'use client';
|
|
10
10
|
|
|
11
|
-
import React
|
|
11
|
+
import React from 'react';
|
|
12
12
|
|
|
13
13
|
import { Preloader } from '@djangocfg/ui-core/components';
|
|
14
14
|
import { SidebarInset, SidebarProvider } from '@djangocfg/ui-core/components';
|
|
15
15
|
|
|
16
|
-
import type { LayoutVisualConfig } from '../types';
|
|
17
16
|
import { PrivateContent, PrivateSidebar } from './components';
|
|
18
17
|
import { useAuthGuard } from './hooks';
|
|
19
18
|
import { useLayoutVisual } from './hooks';
|
|
20
19
|
import { useSidebarDefaultOpen } from './hooks';
|
|
21
20
|
|
|
22
|
-
import type {
|
|
23
|
-
HeaderConfig,
|
|
24
|
-
PrivateLayoutProps,
|
|
25
|
-
SidebarConfig,
|
|
26
|
-
} from './types';
|
|
21
|
+
import type { PrivateLayoutProps } from './types';
|
|
27
22
|
|
|
28
23
|
export type {
|
|
29
24
|
SidebarItem,
|
|
@@ -219,7 +219,7 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
|
|
|
219
219
|
// Always open UPWARD over the avatar (Claude pattern). The footer sits at the
|
|
220
220
|
// bottom of the rail in both expanded and collapsed states, so `top` reads
|
|
221
221
|
// correctly either way (vs. `right`, which floated off to the side).
|
|
222
|
-
const dropdownSide
|
|
222
|
+
const dropdownSide = 'top' as const;
|
|
223
223
|
const avatarClass = cn(
|
|
224
224
|
'h-9 w-9 shrink-0 border border-transparent transition-colors',
|
|
225
225
|
'group-hover:border-sidebar-border/70',
|
|
@@ -17,6 +17,8 @@ import { usePrivateLayoutContext } from '../context';
|
|
|
17
17
|
import { useShellVisualState } from '../hooks';
|
|
18
18
|
import { SidebarBrandSwitcher } from './SidebarBrandSwitcher';
|
|
19
19
|
|
|
20
|
+
const HEADER_ROW_CLASS = 'flex items-center gap-2 mb-2';
|
|
21
|
+
|
|
20
22
|
function SidebarBrandRaw() {
|
|
21
23
|
const { header, homeHref, brandTitle, brandMonogram, isMobile } =
|
|
22
24
|
usePrivateLayoutContext();
|
|
@@ -39,14 +41,7 @@ function SidebarBrandRaw() {
|
|
|
39
41
|
|
|
40
42
|
const customBrand = header?.brand;
|
|
41
43
|
|
|
42
|
-
const headerRowClass =
|
|
43
|
-
() =>
|
|
44
|
-
cn(
|
|
45
|
-
'flex items-center gap-2 mb-2',
|
|
46
|
-
// content.showLabels ? 'px-2' : 'px-1.5',
|
|
47
|
-
),
|
|
48
|
-
[content.showLabels],
|
|
49
|
-
);
|
|
44
|
+
const headerRowClass = HEADER_ROW_CLASS;
|
|
50
45
|
|
|
51
46
|
const expandedHeader = useMemo(
|
|
52
47
|
() => (
|
|
@@ -15,17 +15,21 @@ import { LucideIcon } from '../../../components';
|
|
|
15
15
|
|
|
16
16
|
import type { SidebarFeaturedConfig } from '../types';
|
|
17
17
|
|
|
18
|
+
// The accent names are the public prop's vocabulary; the status tokens they map
|
|
19
|
+
// to carry the meaning and already define both themes, so no `dark:` pair here.
|
|
18
20
|
const ACCENT_CLASS: Record<NonNullable<SidebarFeaturedConfig['accent']>, string> = {
|
|
19
|
-
green: 'bg-
|
|
20
|
-
blue: 'bg-
|
|
21
|
-
amber: 'bg-
|
|
21
|
+
green: 'bg-success/10 text-success-foreground hover:bg-success/15',
|
|
22
|
+
blue: 'bg-info/10 text-info-foreground hover:bg-info/15',
|
|
23
|
+
amber: 'bg-warning/10 text-warning-foreground hover:bg-warning/15',
|
|
22
24
|
primary: 'bg-primary/10 text-primary hover:bg-primary/15',
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
// Solid fill, so the text is the on-fill contrast color — `*-foreground` is the
|
|
28
|
+
// tint for the soft surface above and fails contrast on the fill.
|
|
25
29
|
const BADGE_ACCENT_CLASS: Record<NonNullable<SidebarFeaturedConfig['accent']>, string> = {
|
|
26
|
-
green: 'bg-
|
|
27
|
-
blue: 'bg-
|
|
28
|
-
amber: 'bg-
|
|
30
|
+
green: 'bg-success text-on-success',
|
|
31
|
+
blue: 'bg-info text-on-info',
|
|
32
|
+
amber: 'bg-warning text-on-warning',
|
|
29
33
|
primary: 'bg-primary text-primary-foreground',
|
|
30
34
|
};
|
|
31
35
|
|
|
@@ -156,7 +156,6 @@ export function UserMenu({
|
|
|
156
156
|
|
|
157
157
|
// Authenticated user
|
|
158
158
|
const displayName = user.display_username || user.email || 'User';
|
|
159
|
-
const userInitial = displayName.charAt(0).toUpperCase();
|
|
160
159
|
const userAvatar = user.avatar || '';
|
|
161
160
|
|
|
162
161
|
if (variant === 'mobile') {
|
|
@@ -40,11 +40,15 @@ export interface MobileDrawerShellProps {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Module-level so the "no navigation" case keeps one identity: a fresh `[]` per
|
|
44
|
+
// render would make `mobileNavigation`'s memo recompute on every render.
|
|
45
|
+
const NO_NAVIGATION: NavigationItem[] = [];
|
|
46
|
+
|
|
43
47
|
export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
44
48
|
const context = usePublicLayoutOptional();
|
|
45
49
|
const mobileMenuOpen = props.isOpen ?? context?.mobileMenuOpen ?? false;
|
|
46
50
|
const closeMobileMenu = props.onClose ?? context?.closeMobileMenu ?? (() => {});
|
|
47
|
-
const navigation = props.navigation ??
|
|
51
|
+
const navigation = props.navigation ?? NO_NAVIGATION;
|
|
48
52
|
const userMenu = props.userMenu;
|
|
49
53
|
|
|
50
54
|
const { isAuthenticated, user, routes } = useAuth();
|
|
@@ -259,10 +259,13 @@ function NavbarShellRaw(props: NavbarShellProps) {
|
|
|
259
259
|
// this being present — same contract `NavControls` applies internally.
|
|
260
260
|
const i18n = useLayoutI18nOptional();
|
|
261
261
|
|
|
262
|
-
// Close dropdowns when switching to mobile
|
|
262
|
+
// Close dropdowns when switching to mobile. Depends on `closeDropdown` alone,
|
|
263
|
+
// not on `dropdown`: the object changes identity whenever a menu opens, and
|
|
264
|
+
// depending on it would close the dropdown the moment it is opened.
|
|
265
|
+
const { closeDropdown } = dropdown;
|
|
263
266
|
useEffect(() => {
|
|
264
|
-
if (isTabletOrBelow)
|
|
265
|
-
}, [isTabletOrBelow,
|
|
267
|
+
if (isTabletOrBelow) closeDropdown();
|
|
268
|
+
}, [isTabletOrBelow, closeDropdown]);
|
|
266
269
|
|
|
267
270
|
// Sync navbar surface into context
|
|
268
271
|
const setNavbarSurface = context?.setNavbarSurface;
|
|
@@ -21,7 +21,7 @@ import type { ErrorBoundaryConfig, ErrorTrackingConfig } from '../../components/
|
|
|
21
21
|
export type NextIntlRouting = object & { __nextIntlRouting?: never };
|
|
22
22
|
|
|
23
23
|
// Import local provider configs
|
|
24
|
-
import type { ThemeConfig, SWRConfigOptions
|
|
24
|
+
import type { ThemeConfig, SWRConfigOptions } from './providers.types';
|
|
25
25
|
import type { DevtoolsProps } from '@djangocfg/devtools';
|
|
26
26
|
|
|
27
27
|
// ============================================================================
|
|
@@ -49,9 +49,6 @@ export interface BaseLayoutProps {
|
|
|
49
49
|
/** Analytics configuration (from snippets/Analytics) */
|
|
50
50
|
analytics?: AnalyticsConfig;
|
|
51
51
|
|
|
52
|
-
/** Centrifugo configuration */
|
|
53
|
-
centrifugo?: CentrifugoConfig;
|
|
54
|
-
|
|
55
52
|
/** Error tracking configuration (from components/errors) */
|
|
56
53
|
errorTracking?: ErrorTrackingConfig;
|
|
57
54
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provider Configuration Types
|
|
3
3
|
*
|
|
4
|
-
* Configuration types for layout providers (theme, SWR, MCP chat
|
|
4
|
+
* Configuration types for layout providers (theme, SWR, MCP chat)
|
|
5
5
|
* Note: Analytics, PWA, Push, and Error types are defined in their respective modules
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -25,16 +25,3 @@ export interface SWRConfigOptions {
|
|
|
25
25
|
revalidateOnReconnect?: boolean;
|
|
26
26
|
dedupingInterval?: number;
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
// ============================================================================
|
|
30
|
-
// Centrifugo Configuration
|
|
31
|
-
// ============================================================================
|
|
32
|
-
|
|
33
|
-
export interface CentrifugoConfig {
|
|
34
|
-
/** Enable Centrifugo (default: true) */
|
|
35
|
-
enabled?: boolean;
|
|
36
|
-
/** Auto-connect on mount (default: false) */
|
|
37
|
-
autoConnect?: boolean;
|
|
38
|
-
/** Centrifugo server URL */
|
|
39
|
-
url?: string;
|
|
40
|
-
}
|
|
@@ -42,18 +42,18 @@ const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className = "" }) => {
|
|
|
42
42
|
{breadcrumbs.map((item: BreadcrumbItem, index: number) => (
|
|
43
43
|
<li key={item.path} className="flex items-center">
|
|
44
44
|
{index > 0 && (
|
|
45
|
-
<ChevronRight className="w-4 h-4 text-
|
|
45
|
+
<ChevronRight className="w-4 h-4 text-muted-foreground mx-2" />
|
|
46
46
|
)}
|
|
47
47
|
|
|
48
48
|
{item.isActive ? (
|
|
49
|
-
<span className="text-
|
|
49
|
+
<span className="text-foreground font-medium flex items-center gap-1">
|
|
50
50
|
{index === 0 && <Home className="w-4 h-4" />}
|
|
51
51
|
{item.label}
|
|
52
52
|
</span>
|
|
53
53
|
) : (
|
|
54
54
|
<Link
|
|
55
55
|
href={item.path}
|
|
56
|
-
className="text-
|
|
56
|
+
className="text-muted-foreground hover:text-foreground
|
|
57
57
|
transition-colors duration-200 flex items-center gap-1 hover:underline"
|
|
58
58
|
>
|
|
59
59
|
{index === 0 && <Home className="w-4 h-4" />}
|