@djangocfg/layouts 1.2.31 → 1.2.33

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.
Files changed (28) hide show
  1. package/package.json +5 -5
  2. package/src/auth/context/AccountsContext.tsx +40 -9
  3. package/src/auth/context/AuthContext.tsx +43 -13
  4. package/src/auth/context/types.ts +1 -1
  5. package/src/auth/hooks/useAuthForm.ts +2 -1
  6. package/src/auth/hooks/useAutoAuth.ts +2 -1
  7. package/src/auth/hooks/useLocalStorage.ts +19 -18
  8. package/src/auth/hooks/useProfileCache.ts +4 -1
  9. package/src/auth/hooks/useSessionStorage.ts +19 -18
  10. package/src/index.ts +4 -1
  11. package/src/layouts/AppLayout/AppLayout.tsx +9 -2
  12. package/src/layouts/AppLayout/components/ErrorBoundary.tsx +3 -2
  13. package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
  14. package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +56 -19
  15. package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +29 -18
  16. package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +65 -19
  17. package/src/layouts/AppLayout/providers/CoreProviders.tsx +12 -2
  18. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +2 -1
  19. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -1
  20. package/src/layouts/UILayout/components/layout/Header/Header.tsx +11 -4
  21. package/src/layouts/UILayout/components/layout/Header/HeaderDesktop.tsx +14 -7
  22. package/src/layouts/UILayout/components/layout/Header/TestValidationButton.tsx +265 -0
  23. package/src/layouts/UILayout/components/layout/Header/index.ts +2 -0
  24. package/src/utils/logger.ts +3 -1
  25. package/src/validation/README.md +507 -0
  26. package/src/validation/ValidationErrorContext.tsx +333 -0
  27. package/src/validation/ValidationErrorToast.tsx +251 -0
  28. package/src/validation/index.ts +25 -0
@@ -16,36 +16,36 @@ export interface PackageInfo {
16
16
  /**
17
17
  * Package versions registry
18
18
  * Auto-synced from package.json files
19
- * Last updated: 2025-11-09T10:05:41.931Z
19
+ * Last updated: 2025-11-10T16:46:04.571Z
20
20
  */
21
21
  const PACKAGE_VERSIONS: PackageInfo[] = [
22
22
  {
23
23
  "name": "@djangocfg/ui",
24
- "version": "1.2.31"
24
+ "version": "1.2.33"
25
25
  },
26
26
  {
27
27
  "name": "@djangocfg/api",
28
- "version": "1.2.31"
28
+ "version": "1.2.33"
29
29
  },
30
30
  {
31
31
  "name": "@djangocfg/layouts",
32
- "version": "1.2.31"
32
+ "version": "1.2.33"
33
33
  },
34
34
  {
35
35
  "name": "@djangocfg/markdown",
36
- "version": "1.2.31"
36
+ "version": "1.2.33"
37
37
  },
38
38
  {
39
39
  "name": "@djangocfg/og-image",
40
- "version": "1.2.31"
40
+ "version": "1.2.33"
41
41
  },
42
42
  {
43
43
  "name": "@djangocfg/eslint-config",
44
- "version": "1.2.31"
44
+ "version": "1.2.33"
45
45
  },
46
46
  {
47
47
  "name": "@djangocfg/typescript-config",
48
- "version": "1.2.31"
48
+ "version": "1.2.33"
49
49
  }
50
50
  ];
51
51
 
@@ -118,28 +118,65 @@ function AdminLayoutClient({
118
118
  const { isAdminUser, user, isLoading, isAuthenticated, loadCurrentProfile } = useAuth();
119
119
  // console.log('[AdminLayout] Rendering with user:', user, 'isLoading:', isLoading, 'isAuthenticated:', isAuthenticated);
120
120
 
121
+ // Use refs to prevent re-renders and maintain state across renders
122
+ const profileLoadedRef = React.useRef(false);
123
+ const tokensReceivedRef = React.useRef(false);
124
+ const loadCurrentProfileRef = React.useRef(loadCurrentProfile);
125
+
126
+ // Update ref when loadCurrentProfile changes (but don't trigger re-render)
127
+ React.useEffect(() => {
128
+ loadCurrentProfileRef.current = loadCurrentProfile;
129
+ }, [loadCurrentProfile]);
130
+
131
+ // Create a STABLE callback that NEVER changes (no dependencies)
132
+ // Use refs to access latest values without recreating the callback
133
+ const handleAuthToken = React.useCallback(async (authToken: string, refreshToken?: string) => {
134
+ console.log('[AdminLayout] handleAuthToken called, tokensReceived:', tokensReceivedRef.current, 'profileLoaded:', profileLoadedRef.current);
135
+
136
+ // Prevent duplicate token processing
137
+ if (tokensReceivedRef.current) {
138
+ console.log('[AdminLayout] Tokens already received and processed, ignoring duplicate');
139
+ return;
140
+ }
141
+
142
+ // Mark tokens as received IMMEDIATELY to prevent race conditions
143
+ tokensReceivedRef.current = true;
144
+
145
+ console.log('[AdminLayout] First time receiving tokens, processing...');
146
+ console.log('[AdminLayout] authToken:', authToken.substring(0, 20) + '...', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
147
+
148
+ // Always set tokens in API client
149
+ api.setToken(authToken, refreshToken);
150
+ console.log('[AdminLayout] Tokens set in API client');
151
+
152
+ // Load user profile after setting tokens - ONLY ONCE per session
153
+ if (!profileLoadedRef.current) {
154
+ console.log('[AdminLayout] Loading user profile (first time)...');
155
+ try {
156
+ await loadCurrentProfileRef.current('AdminLayout.onAuthTokenReceived');
157
+ profileLoadedRef.current = true;
158
+ console.log('[AdminLayout] User profile loaded successfully');
159
+ } catch (error) {
160
+ console.error('[AdminLayout] Failed to load profile:', error);
161
+ // Reset flags on error so user can retry
162
+ tokensReceivedRef.current = false;
163
+ profileLoadedRef.current = false;
164
+ }
165
+ } else {
166
+ console.log('[AdminLayout] Profile already loaded, skipping duplicate call');
167
+ }
168
+
169
+ // Call custom handler if provided
170
+ if (config?.onAuthTokenReceived) {
171
+ console.log('[AdminLayout] Calling custom onAuthTokenReceived handler');
172
+ config.onAuthTokenReceived(authToken, refreshToken);
173
+ }
174
+ }, []); // ← EMPTY DEPS - callback NEVER changes
175
+
121
176
  // useCfgApp hook is called here to initialize iframe communication
122
177
  // Automatically sets tokens in API client when received from parent
123
178
  const { isEmbedded } = useCfgApp({
124
- onAuthTokenReceived: async (authToken, refreshToken) => {
125
- // console.log('[AdminLayout] onAuthTokenReceived called');
126
- // console.log('[AdminLayout] authToken:', authToken.substring(0, 20) + '...', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
127
-
128
- // Always set tokens in API client
129
- api.setToken(authToken, refreshToken);
130
- // console.log('[AdminLayout] Tokens set in API client');
131
-
132
- // Load user profile after setting tokens
133
- // console.log('[AdminLayout] Loading user profile...');
134
- await loadCurrentProfile();
135
- // console.log('[AdminLayout] User profile loaded');
136
-
137
- // Call custom handler if provided
138
- if (config?.onAuthTokenReceived) {
139
- // console.log('[AdminLayout] Calling custom onAuthTokenReceived handler');
140
- config.onAuthTokenReceived(authToken, refreshToken);
141
- }
142
- }
179
+ onAuthTokenReceived: handleAuthToken // Now truly stable - never recreated
143
180
  });
144
181
 
145
182
  // console.log('[AdminLayout] isEmbedded:', isEmbedded);
@@ -13,6 +13,7 @@ import { useRouter } from 'next/router';
13
13
  import { useAuth } from '../../../../../auth';
14
14
  import { useThemeContext } from '@djangocfg/ui';
15
15
  import { useCfgApp } from '../hooks/useApp';
16
+ import { authLogger } from '../../../../../utils/logger';
16
17
 
17
18
  /**
18
19
  * ParentSync Component
@@ -65,7 +66,7 @@ function ParentSyncClient() {
65
66
  }
66
67
  }, [isEmbedded, parentTheme, setTheme]);
67
68
 
68
- // 2. Send auth status from iframe → parent
69
+ // 2. Send auth status from iframe → parent (debounced to prevent spam)
69
70
  useEffect(() => {
70
71
  // Only send if embedded and mounted
71
72
  if (!isEmbedded || !isMounted) {
@@ -76,24 +77,34 @@ function ParentSyncClient() {
76
77
  return;
77
78
  }
78
79
 
79
- const authData = {
80
- isAuthenticated: auth.isAuthenticated,
81
- isLoading: auth.isLoading,
82
- hasUser: !!auth.user
83
- };
80
+ // Use primitive values to avoid unnecessary re-renders from object reference changes
81
+ const isAuthenticated = auth.isAuthenticated;
82
+ const isLoading = auth.isLoading;
83
+ const hasUser = !!auth.user;
84
84
 
85
- // console.log('[ParentSync] 📤 Sending auth status to parent:', authData);
85
+ // console.log('[ParentSync] 📤 Sending auth status to parent:', { isAuthenticated, isLoading, hasUser });
86
86
 
87
- try {
88
- window.parent.postMessage({
89
- type: 'iframe-auth-status',
90
- data: authData
91
- }, '*');
92
- // console.log('[ParentSync] Auth status sent successfully');
93
- } catch (e) {
94
- console.error('[ParentSync] Failed to send auth status:', e);
95
- }
96
- }, [auth.isAuthenticated, auth.isLoading, auth.user, isEmbedded, isMounted]);
87
+ // Debounce the postMessage to prevent excessive calls
88
+ const timeoutId = setTimeout(() => {
89
+ try {
90
+ window.parent.postMessage({
91
+ type: 'iframe-auth-status',
92
+ data: { isAuthenticated, isLoading, hasUser }
93
+ }, '*');
94
+ // authLogger.debug('Auth status sent successfully');
95
+ } catch (e) {
96
+ authLogger.error('Failed to send auth status:', e);
97
+ }
98
+ }, 100); // 100ms debounce
99
+
100
+ return () => clearTimeout(timeoutId);
101
+ }, [
102
+ auth.isAuthenticated,
103
+ auth.isLoading,
104
+ !!auth.user, // Convert to boolean to prevent object reference changes
105
+ isEmbedded,
106
+ isMounted
107
+ ]);
97
108
 
98
109
  // 3. iframe-resize removed - was causing log spam
99
110
 
@@ -111,7 +122,7 @@ function ParentSyncClient() {
111
122
  data: { path: url }
112
123
  }, '*');
113
124
  } catch (e) {
114
- console.error('[ParentSync] Failed to send navigation event:', e);
125
+ authLogger.error('Failed to send navigation event:', e);
115
126
  }
116
127
  };
117
128
 
@@ -4,8 +4,10 @@
4
4
 
5
5
  'use client';
6
6
 
7
+ import React from 'react';
7
8
  import { useState, useEffect } from 'react';
8
9
  import { useRouter } from 'next/router';
10
+ import { authLogger } from '../../../../../utils/logger';
9
11
 
10
12
  export interface UseCfgAppReturn {
11
13
  /**
@@ -93,6 +95,14 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
93
95
  const [parentTheme, setParentTheme] = useState<'light' | 'dark' | null>(null);
94
96
  const [parentThemeMode, setParentThemeMode] = useState<string | null>(null);
95
97
 
98
+ // Store callback in ref OUTSIDE useEffect so it's always available
99
+ const callbackRef = React.useRef(options?.onAuthTokenReceived);
100
+
101
+ // Update callback ref when options change (but don't re-register listener)
102
+ React.useEffect(() => {
103
+ callbackRef.current = options?.onAuthTokenReceived;
104
+ }, [options?.onAuthTokenReceived]);
105
+
96
106
  useEffect(() => {
97
107
  setIsMounted(true);
98
108
 
@@ -109,6 +119,11 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
109
119
  setReferrer(document.referrer);
110
120
  }
111
121
 
122
+ // Debounce timeout for parent-auth messages
123
+ let authTokenTimeout: NodeJS.Timeout | null = null;
124
+ // Track if we've already processed auth tokens
125
+ let authTokenProcessed = false;
126
+
112
127
  // Listen for messages from parent window
113
128
  const handleMessage = (event: MessageEvent) => {
114
129
  // console.log('[useCfgApp] RAW message event:', {
@@ -123,20 +138,47 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
123
138
 
124
139
  switch (type) {
125
140
  case 'parent-auth':
126
- // console.log('[useCfgApp] parent-auth message received');
127
- // Receive authentication tokens from parent
128
- if (data?.authToken && options?.onAuthTokenReceived) {
129
- // console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
130
- // console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
131
- try {
132
- options.onAuthTokenReceived(data.authToken, data.refreshToken);
133
- // console.log('[useCfgApp] onAuthTokenReceived callback completed successfully');
134
- } catch (e) {
135
- console.error('[useCfgApp] Failed to process auth tokens:', e);
136
- }
137
- } else {
138
- console.warn('[useCfgApp] parent-auth message received but authToken or callback missing:', { hasToken: !!data?.authToken, hasCallback: !!options?.onAuthTokenReceived });
141
+ console.log('[useCfgApp] parent-auth message received, authTokenProcessed:', authTokenProcessed);
142
+
143
+ // Prevent processing if already handled
144
+ if (authTokenProcessed) {
145
+ console.log('[useCfgApp] Auth tokens already processed, ignoring duplicate message');
146
+ return;
147
+ }
148
+
149
+ // Cancel previous timeout to debounce rapid auth messages
150
+ if (authTokenTimeout) {
151
+ clearTimeout(authTokenTimeout);
139
152
  }
153
+
154
+ // Debounce auth token processing to prevent rapid calls (300ms)
155
+ authTokenTimeout = setTimeout(() => {
156
+ // Double-check still not processed (race condition protection)
157
+ if (authTokenProcessed) {
158
+ console.log('[useCfgApp] Auth tokens already processed during debounce, skipping');
159
+ return;
160
+ }
161
+
162
+ // Receive authentication tokens from parent
163
+ if (data?.authToken && callbackRef.current) {
164
+ console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
165
+ console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
166
+
167
+ // Mark as processed BEFORE calling callback
168
+ authTokenProcessed = true;
169
+
170
+ try {
171
+ callbackRef.current(data.authToken, data.refreshToken);
172
+ console.log('[useCfgApp] onAuthTokenReceived callback completed successfully');
173
+ } catch (e) {
174
+ authLogger.error('Failed to process auth tokens:', e);
175
+ // Reset on error to allow retry
176
+ authTokenProcessed = false;
177
+ }
178
+ } else {
179
+ authLogger.warn('parent-auth message received but authToken or callback missing:', { hasToken: !!data?.authToken, hasCallback: !!callbackRef.current });
180
+ }
181
+ }, 300); // 300ms debounce
140
182
  break;
141
183
 
142
184
  case 'parent-theme':
@@ -148,9 +190,9 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
148
190
  if (data.themeMode) {
149
191
  setParentThemeMode(data.themeMode);
150
192
  }
151
- // console.log('[useCfgApp] Theme set successfully:', data.theme);
193
+ // authLogger.debug('Theme set successfully:', data.theme);
152
194
  } catch (e) {
153
- console.error('[useCfgApp] Failed to process theme:', e);
195
+ authLogger.error('Failed to process theme:', e);
154
196
  }
155
197
  }
156
198
  break;
@@ -174,9 +216,9 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
174
216
  referrer: document.referrer
175
217
  }
176
218
  }, '*');
177
- // console.log('[useCfgApp] iframe-ready message sent');
219
+ // authLogger.debug('iframe-ready message sent');
178
220
  } catch (e) {
179
- console.error('[useCfgApp] Failed to notify parent about ready state:', e);
221
+ authLogger.error('Failed to notify parent about ready state:', e);
180
222
  }
181
223
  } else {
182
224
  // console.log('[useCfgApp] Not in iframe, skipping iframe-ready message');
@@ -184,8 +226,12 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
184
226
 
185
227
  return () => {
186
228
  window.removeEventListener('message', handleMessage);
229
+ // Clear timeout on cleanup
230
+ if (authTokenTimeout) {
231
+ clearTimeout(authTokenTimeout);
232
+ }
187
233
  };
188
- }, [options]);
234
+ }, []); // ← EMPTY DEPS - register listener ONCE
189
235
 
190
236
  // Notify parent about route changes
191
237
  useEffect(() => {
@@ -200,7 +246,7 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
200
246
  }
201
247
  }, '*');
202
248
  } catch (e) {
203
- console.error('[iframe] Failed to notify parent about navigation:', e);
249
+ authLogger.error('Failed to notify parent about navigation:', e);
204
250
  }
205
251
  }, [router.asPath, router.pathname, isEmbedded, isMounted]);
206
252
 
@@ -9,11 +9,18 @@
9
9
  import React, { ReactNode } from 'react';
10
10
  import { ThemeProvider, Toaster } from '@djangocfg/ui';
11
11
  import { AuthProvider } from '../../../auth';
12
+ import { ValidationErrorProvider } from '../../../validation';
12
13
  import type { AppLayoutConfig } from '../types';
14
+ import type { ValidationErrorConfig } from '../../../validation';
13
15
 
14
16
  export interface CoreProvidersProps {
15
17
  children: ReactNode;
16
18
  config: AppLayoutConfig;
19
+ /**
20
+ * Configuration for validation error tracking
21
+ * @default { enableToast: true, maxErrors: 50 }
22
+ */
23
+ validationConfig?: Partial<ValidationErrorConfig>;
17
24
  }
18
25
 
19
26
  /**
@@ -22,9 +29,10 @@ export interface CoreProvidersProps {
22
29
  * Provides:
23
30
  * - ThemeProvider (dark/light mode)
24
31
  * - AuthProvider (authentication)
32
+ * - ValidationErrorProvider (Zod validation error tracking)
25
33
  * - Toaster (notifications)
26
34
  */
27
- export function CoreProviders({ children, config }: CoreProvidersProps) {
35
+ export function CoreProviders({ children, config, validationConfig }: CoreProvidersProps) {
28
36
  return (
29
37
  <ThemeProvider>
30
38
  <AuthProvider
@@ -37,7 +45,9 @@ export function CoreProviders({ children, config }: CoreProvidersProps) {
37
45
  },
38
46
  }}
39
47
  >
40
- {children}
48
+ <ValidationErrorProvider config={validationConfig}>
49
+ {children}
50
+ </ValidationErrorProvider>
41
51
  </AuthProvider>
42
52
 
43
53
  {/* Global toast notifications */}
@@ -36,6 +36,7 @@ import { z } from 'zod';
36
36
  import { usePaymentsContext, useRootPaymentsContext } from '@djangocfg/api/cfg/contexts';
37
37
  import { PAYMENT_EVENTS, closePaymentsDialog } from '../events';
38
38
  import { openPaymentDetailsDialog } from '../events';
39
+ import { paymentsLogger } from '../../../utils/logger';
39
40
 
40
41
  // Payment creation schema
41
42
  const PaymentCreateSchema = z.object({
@@ -139,7 +140,7 @@ export const CreatePaymentDialog: React.FC = () => {
139
140
  openPaymentDetailsDialog(String(paymentId));
140
141
  }
141
142
  } catch (error) {
142
- console.error('Failed to create payment:', error);
143
+ paymentsLogger.error('Failed to create payment:', error);
143
144
  } finally {
144
145
  setIsSubmitting(false);
145
146
  }
@@ -7,6 +7,7 @@ import { toast } from 'sonner';
7
7
  import { Avatar, AvatarFallback, Button } from '@djangocfg/ui/components';
8
8
  import { useAccountsContext } from '@djangocfg/layouts/auth/context';
9
9
  import { useAuth } from '../../../auth';
10
+ import { profileLogger } from '../../../utils/logger';
10
11
 
11
12
  export const AvatarSection = () => {
12
13
  const { user } = useAuth();
@@ -58,7 +59,7 @@ export const AvatarSection = () => {
58
59
  setAvatarPreview(null);
59
60
  } catch (error) {
60
61
  toast.error('Failed to upload avatar');
61
- console.error('Avatar upload error:', error);
62
+ profileLogger.error('Avatar upload error:', error);
62
63
  } finally {
63
64
  setIsUploading(false);
64
65
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import React from 'react';
9
9
  import { CopyAIButton } from './CopyAIButton';
10
+ import { TestValidationButton } from './TestValidationButton';
10
11
 
11
12
  export interface HeaderProps {
12
13
  /** Page title */
@@ -42,10 +43,16 @@ export function Header({
42
43
  </div>
43
44
  </div>
44
45
 
45
- {/* Right: Copy for AI Button */}
46
- {onCopyForAI && (
47
- <CopyAIButton onCopyForAI={onCopyForAI} />
48
- )}
46
+ {/* Right: Developer Tools */}
47
+ <div className="flex items-center gap-2">
48
+ {/* Test Validation Error Button (dev only) */}
49
+ <TestValidationButton size="sm" />
50
+
51
+ {/* Copy for AI Button */}
52
+ {onCopyForAI && (
53
+ <CopyAIButton onCopyForAI={onCopyForAI} />
54
+ )}
55
+ </div>
49
56
  </div>
50
57
  </div>
51
58
  </header>
@@ -7,6 +7,7 @@
7
7
 
8
8
  import React from 'react';
9
9
  import { CopyAIButton } from './CopyAIButton';
10
+ import { TestValidationButton } from './TestValidationButton';
10
11
 
11
12
  interface HeaderDesktopProps {
12
13
  /** Page title */
@@ -31,13 +32,19 @@ export function HeaderDesktop({
31
32
  <h1 className="text-lg font-semibold">{title}</h1>
32
33
  </div>
33
34
 
34
- {/* Copy for AI Button */}
35
- <CopyAIButton
36
- onCopyForAI={onCopyForAI}
37
- size="sm"
38
- showLabel={true}
39
- className="gap-2"
40
- />
35
+ {/* Developer Tools */}
36
+ <div className="flex items-center gap-2">
37
+ {/* Test Validation Error Button (dev only) */}
38
+ <TestValidationButton size="sm" />
39
+
40
+ {/* Copy for AI Button */}
41
+ <CopyAIButton
42
+ onCopyForAI={onCopyForAI}
43
+ size="sm"
44
+ showLabel={true}
45
+ className="gap-2"
46
+ />
47
+ </div>
41
48
  </div>
42
49
  </div>
43
50
  );