@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.
- package/package.json +5 -5
- package/src/auth/context/AccountsContext.tsx +40 -9
- package/src/auth/context/AuthContext.tsx +43 -13
- package/src/auth/context/types.ts +1 -1
- package/src/auth/hooks/useAuthForm.ts +2 -1
- package/src/auth/hooks/useAutoAuth.ts +2 -1
- package/src/auth/hooks/useLocalStorage.ts +19 -18
- package/src/auth/hooks/useProfileCache.ts +4 -1
- package/src/auth/hooks/useSessionStorage.ts +19 -18
- package/src/index.ts +4 -1
- package/src/layouts/AppLayout/AppLayout.tsx +9 -2
- package/src/layouts/AppLayout/components/ErrorBoundary.tsx +3 -2
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +56 -19
- package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +29 -18
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +65 -19
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +12 -2
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +2 -1
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -1
- package/src/layouts/UILayout/components/layout/Header/Header.tsx +11 -4
- package/src/layouts/UILayout/components/layout/Header/HeaderDesktop.tsx +14 -7
- package/src/layouts/UILayout/components/layout/Header/TestValidationButton.tsx +265 -0
- package/src/layouts/UILayout/components/layout/Header/index.ts +2 -0
- package/src/utils/logger.ts +3 -1
- package/src/validation/README.md +507 -0
- package/src/validation/ValidationErrorContext.tsx +333 -0
- package/src/validation/ValidationErrorToast.tsx +251 -0
- package/src/validation/index.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.33",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"check": "tsc --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@djangocfg/api": "^1.2.
|
|
67
|
-
"@djangocfg/og-image": "^1.2.
|
|
68
|
-
"@djangocfg/ui": "^1.2.
|
|
66
|
+
"@djangocfg/api": "^1.2.33",
|
|
67
|
+
"@djangocfg/og-image": "^1.2.33",
|
|
68
|
+
"@djangocfg/ui": "^1.2.33",
|
|
69
69
|
"@hookform/resolvers": "^5.2.0",
|
|
70
70
|
"consola": "^3.4.2",
|
|
71
71
|
"lucide-react": "^0.468.0",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"vidstack": "0.6.15"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@djangocfg/typescript-config": "^1.2.
|
|
89
|
+
"@djangocfg/typescript-config": "^1.2.33",
|
|
90
90
|
"@types/node": "^24.7.2",
|
|
91
91
|
"@types/react": "19.2.2",
|
|
92
92
|
"@types/react-dom": "19.2.1",
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
"use client";
|
|
15
15
|
|
|
16
|
-
import { createContext, useContext, ReactNode, useState, useCallback } from 'react';
|
|
16
|
+
import { createContext, useContext, ReactNode, useState, useCallback, useRef, useEffect } from 'react';
|
|
17
17
|
import { getCachedProfile, setCachedProfile, clearProfileCache } from '../hooks/useProfileCache';
|
|
18
|
+
import { authLogger } from '../../utils/logger';
|
|
18
19
|
import {
|
|
19
20
|
api,
|
|
20
21
|
usePartialUpdateAccountsProfilePartialUpdate,
|
|
@@ -54,7 +55,7 @@ export interface AccountsContextValue {
|
|
|
54
55
|
updateProfile: (data: UserProfileUpdateRequest) => Promise<User>;
|
|
55
56
|
partialUpdateProfile: (data: PatchedUserProfileUpdateRequest) => Promise<User>;
|
|
56
57
|
uploadAvatar: (formData: FormData) => Promise<User>;
|
|
57
|
-
refreshProfile: () => Promise<User | undefined>;
|
|
58
|
+
refreshProfile: (callerId?: string) => Promise<User | undefined>;
|
|
58
59
|
|
|
59
60
|
// Authentication
|
|
60
61
|
requestOTP: (data: OTPRequestRequest) => Promise<OTPRequestResponse>;
|
|
@@ -87,6 +88,19 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
87
88
|
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
|
|
88
89
|
const [profileError, setProfileError] = useState<Error | null>(null);
|
|
89
90
|
|
|
91
|
+
// Use refs to access current state without adding to dependencies
|
|
92
|
+
const profileRef = useRef<User | undefined>(profile);
|
|
93
|
+
const isLoadingRef = useRef(false);
|
|
94
|
+
|
|
95
|
+
// Keep refs in sync with state
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
profileRef.current = profile;
|
|
98
|
+
}, [profile]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
isLoadingRef.current = isLoadingProfile;
|
|
102
|
+
}, [isLoadingProfile]);
|
|
103
|
+
|
|
90
104
|
// Mutation hooks
|
|
91
105
|
const updateMutation = useUpdateAccountsProfileUpdateUpdate();
|
|
92
106
|
const partialUpdateMutation = usePartialUpdateAccountsProfilePartialUpdate();
|
|
@@ -95,13 +109,29 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
95
109
|
const otpVerifyMutation = useCreateAccountsOtpVerifyCreate();
|
|
96
110
|
const tokenRefreshMutation = useCreateAccountsTokenRefreshCreate();
|
|
97
111
|
|
|
98
|
-
// Refresh profile - fetch and cache
|
|
99
|
-
const refreshProfile = useCallback(async (): Promise<User | undefined> => {
|
|
112
|
+
// Refresh profile - fetch and cache with stable callback
|
|
113
|
+
const refreshProfile = useCallback(async (callerId?: string): Promise<User | undefined> => {
|
|
114
|
+
// Use refs to check current state without creating dependency
|
|
115
|
+
const currentProfile = profileRef.current;
|
|
116
|
+
const currentLoading = isLoadingRef.current;
|
|
117
|
+
|
|
118
|
+
// Prevent duplicate calls if profile is already loaded and not loading
|
|
119
|
+
if (currentProfile && !currentLoading) {
|
|
120
|
+
authLogger.debug(`Profile already loaded, returning cached (caller: ${callerId})`);
|
|
121
|
+
return currentProfile;
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
setIsLoadingProfile(true);
|
|
125
|
+
isLoadingRef.current = true;
|
|
101
126
|
setProfileError(null);
|
|
102
127
|
try {
|
|
128
|
+
// Log caller for debugging excessive API calls using consola logger
|
|
129
|
+
if (callerId) {
|
|
130
|
+
authLogger.debug(`Profile refresh called by: ${callerId}`);
|
|
131
|
+
}
|
|
103
132
|
const result = await getAccountsProfileRetrieve(api as unknown as API);
|
|
104
133
|
setProfile(result);
|
|
134
|
+
profileRef.current = result;
|
|
105
135
|
// Save to cache with 1 hour TTL
|
|
106
136
|
setCachedProfile(result);
|
|
107
137
|
return result;
|
|
@@ -111,27 +141,28 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
111
141
|
throw err;
|
|
112
142
|
} finally {
|
|
113
143
|
setIsLoadingProfile(false);
|
|
144
|
+
isLoadingRef.current = false;
|
|
114
145
|
}
|
|
115
|
-
}, []);
|
|
146
|
+
}, []); // Empty dependencies - callback is stable
|
|
116
147
|
|
|
117
148
|
// Update profile (full)
|
|
118
149
|
const updateProfile = async (data: UserProfileUpdateRequest): Promise<User> => {
|
|
119
150
|
const result = await updateMutation(data, api as unknown as API);
|
|
120
|
-
await refreshProfile();
|
|
151
|
+
await refreshProfile('AccountsContext.updateProfile');
|
|
121
152
|
return result as User;
|
|
122
153
|
};
|
|
123
154
|
|
|
124
155
|
// Partial update profile
|
|
125
156
|
const partialUpdateProfile = async (data: PatchedUserProfileUpdateRequest): Promise<User> => {
|
|
126
157
|
const result = await partialUpdateMutation(data, api as unknown as API);
|
|
127
|
-
await refreshProfile();
|
|
158
|
+
await refreshProfile('AccountsContext.partialUpdateProfile');
|
|
128
159
|
return result as User;
|
|
129
160
|
};
|
|
130
161
|
|
|
131
162
|
// Upload avatar
|
|
132
163
|
const uploadAvatar = async (formData: FormData): Promise<User> => {
|
|
133
164
|
const result = await avatarMutation(formData, api as unknown as API);
|
|
134
|
-
await refreshProfile();
|
|
165
|
+
await refreshProfile('AccountsContext.uploadAvatar');
|
|
135
166
|
return result as User;
|
|
136
167
|
};
|
|
137
168
|
|
|
@@ -149,7 +180,7 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
149
180
|
if (result.access && result.refresh) {
|
|
150
181
|
api.setToken(result.access, result.refresh);
|
|
151
182
|
// Refresh profile to load user data with new token
|
|
152
|
-
await refreshProfile();
|
|
183
|
+
await refreshProfile('AccountsContext.verifyOTP');
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
return result as OTPVerifyResponse;
|
|
@@ -59,6 +59,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
59
59
|
// Use refs to avoid dependency issues
|
|
60
60
|
const userRef = useRef(user);
|
|
61
61
|
const configRef = useRef(config);
|
|
62
|
+
const isLoadingProfileRef = useRef(false);
|
|
62
63
|
|
|
63
64
|
// Update refs when values change
|
|
64
65
|
useEffect(() => {
|
|
@@ -93,43 +94,58 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
93
94
|
return false;
|
|
94
95
|
}, [clearAuthState]);
|
|
95
96
|
|
|
96
|
-
// Simple profile loading without retry - now uses AccountsContext
|
|
97
|
-
const loadCurrentProfile = useCallback(async (): Promise<void> => {
|
|
98
|
-
|
|
97
|
+
// Simple profile loading without retry - now uses AccountsContext with memoization
|
|
98
|
+
const loadCurrentProfile = useCallback(async (callerId?: string): Promise<void> => {
|
|
99
|
+
const finalCallerId = callerId || 'AuthContext.loadCurrentProfile';
|
|
100
|
+
|
|
101
|
+
// Check if profile loading is already in progress
|
|
102
|
+
if (isLoadingProfileRef.current) {
|
|
103
|
+
authLogger.debug(`Profile loading already in progress, skipping duplicate call from: ${finalCallerId}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
authLogger.debug(`loadCurrentProfile called by: ${finalCallerId}`);
|
|
108
|
+
|
|
99
109
|
try {
|
|
110
|
+
isLoadingProfileRef.current = true;
|
|
111
|
+
|
|
100
112
|
// Ensure API clients are properly initialized with current token
|
|
101
113
|
const isAuth = api.isAuthenticated();
|
|
102
114
|
const token = api.getToken();
|
|
103
|
-
//
|
|
115
|
+
// authLogger.debug('isAuthenticated:', isAuth, 'token:', token ? token.substring(0, 20) + '...' : 'null');
|
|
104
116
|
|
|
105
117
|
if (!isAuth) {
|
|
106
|
-
|
|
118
|
+
authLogger.warn('No valid authentication token, throwing error');
|
|
107
119
|
throw new Error('No valid authentication token');
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
// Check if profile is already loaded in AccountsContext to prevent duplicate API calls
|
|
123
|
+
if (accounts.profile && !accounts.isLoadingProfile) {
|
|
124
|
+
authLogger.debug('Profile already loaded in AccountsContext, skipping API call');
|
|
125
|
+
setInitialized(true);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Refresh profile from AccountsContext (now with memoization)
|
|
130
|
+
const refreshedProfile = await accounts.refreshProfile(finalCallerId);
|
|
113
131
|
|
|
114
132
|
if (refreshedProfile) {
|
|
115
|
-
// console.log('[AuthContext] Profile loaded successfully:', refreshedProfile.id, 'is_staff:', refreshedProfile.is_staff, 'is_superuser:', refreshedProfile.is_superuser);
|
|
116
133
|
authLogger.info('Profile loaded successfully:', refreshedProfile.id);
|
|
117
134
|
} else {
|
|
118
|
-
console.warn('[AuthContext] Profile refresh returned undefined - but keeping tokens');
|
|
119
135
|
authLogger.warn('Profile refresh returned undefined - but keeping tokens');
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
// Always mark as initialized if we have valid tokens
|
|
123
139
|
// Don't clear tokens just because profile fetch failed
|
|
124
140
|
setInitialized(true);
|
|
125
|
-
// console.log('[AuthContext] loadCurrentProfile completed, initialized=true');
|
|
126
141
|
} catch (error) {
|
|
127
|
-
console.error('[AuthContext] Failed to load profile:', error);
|
|
128
142
|
authLogger.error('Failed to load profile:', error);
|
|
129
143
|
// Use global error handler first, fallback to clearing state
|
|
130
144
|
if (!handleGlobalAuthError(error, 'loadCurrentProfile')) {
|
|
131
145
|
clearAuthState('loadCurrentProfile:error');
|
|
132
146
|
}
|
|
147
|
+
} finally {
|
|
148
|
+
isLoadingProfileRef.current = false;
|
|
133
149
|
}
|
|
134
150
|
}, [clearAuthState, handleGlobalAuthError, accounts]);
|
|
135
151
|
|
|
@@ -140,6 +156,10 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
140
156
|
const initializeAuth = async () => {
|
|
141
157
|
authLogger.info('Initializing auth...');
|
|
142
158
|
|
|
159
|
+
// Check if running in iframe (AdminLayout will handle auth via postMessage)
|
|
160
|
+
const isInIframe = typeof window !== 'undefined' && window.self !== window.top;
|
|
161
|
+
authLogger.info('Is in iframe:', isInIframe);
|
|
162
|
+
|
|
143
163
|
// Debug token state
|
|
144
164
|
const token = api.getToken();
|
|
145
165
|
const refreshToken = api.getRefreshToken();
|
|
@@ -167,11 +187,21 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
167
187
|
return;
|
|
168
188
|
}
|
|
169
189
|
|
|
190
|
+
// In iframe mode WITHOUT tokens yet - wait for AdminLayout to receive them via postMessage
|
|
191
|
+
// Don't initialize yet - AdminLayout.handleAuthToken will call loadCurrentProfile
|
|
192
|
+
if (isInIframe && !hasTokens) {
|
|
193
|
+
authLogger.info('Running in iframe without tokens - waiting for parent to send via postMessage');
|
|
194
|
+
authLogger.info('AdminLayout will handle auth initialization, skipping AuthContext init');
|
|
195
|
+
setInitialized(true); // Mark as initialized to prevent re-initialization
|
|
196
|
+
setIsLoading(false);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
170
200
|
if (hasTokens) {
|
|
171
201
|
setIsLoading(true);
|
|
172
202
|
try {
|
|
173
203
|
authLogger.info('No cached profile found, loading from API...');
|
|
174
|
-
await loadCurrentProfile();
|
|
204
|
+
await loadCurrentProfile('AuthContext.initializeAuth');
|
|
175
205
|
} catch (error) {
|
|
176
206
|
authLogger.error('Failed to load profile during initialization:', error);
|
|
177
207
|
// If profile loading fails, clear auth state
|
|
@@ -29,7 +29,7 @@ export interface AuthContextType {
|
|
|
29
29
|
isLoading: boolean;
|
|
30
30
|
isAuthenticated: boolean;
|
|
31
31
|
isAdminUser: boolean;
|
|
32
|
-
loadCurrentProfile: () => Promise<void>;
|
|
32
|
+
loadCurrentProfile: (callerId?: string) => Promise<void>;
|
|
33
33
|
checkAuthAndRedirect: () => Promise<void>;
|
|
34
34
|
|
|
35
35
|
// Token Methods
|
|
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
3
3
|
import { useAuth } from '../context';
|
|
4
4
|
import { useAutoAuth } from './useAutoAuth';
|
|
5
5
|
import { useLocalStorage } from './useLocalStorage';
|
|
6
|
+
import { authLogger } from '../../utils/logger';
|
|
6
7
|
|
|
7
8
|
export interface AuthFormState {
|
|
8
9
|
identifier: string; // Email or phone number
|
|
@@ -269,7 +270,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormState & AuthFo
|
|
|
269
270
|
// Auto-detect OTP from URL query parameters
|
|
270
271
|
useAutoAuth({
|
|
271
272
|
onOTPDetected: (otp: string) => {
|
|
272
|
-
|
|
273
|
+
authLogger.info('OTP detected, auto-submitting');
|
|
273
274
|
|
|
274
275
|
// Get saved identifier from auth context
|
|
275
276
|
const savedEmail = getSavedEmail();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRouter } from 'next/router';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
+
import { authLogger } from '../../utils/logger';
|
|
3
4
|
|
|
4
5
|
export interface UseAutoAuthOptions {
|
|
5
6
|
onOTPDetected?: (otp: string) => void;
|
|
@@ -21,7 +22,7 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
|
|
|
21
22
|
|
|
22
23
|
// Handle OTP detection
|
|
23
24
|
if (queryOtp && typeof queryOtp === 'string' && queryOtp.length === 6) {
|
|
24
|
-
|
|
25
|
+
authLogger.info('OTP detected in URL:', queryOtp);
|
|
25
26
|
onOTPDetected?.(queryOtp);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
+
import { authLogger } from '../../utils/logger';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Simple localStorage hook with better error handling
|
|
@@ -27,7 +28,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
27
28
|
return item as T;
|
|
28
29
|
}
|
|
29
30
|
} catch (error) {
|
|
30
|
-
|
|
31
|
+
authLogger.error(`Error reading localStorage key "${key}":`, error);
|
|
31
32
|
return initialValue;
|
|
32
33
|
}
|
|
33
34
|
});
|
|
@@ -41,13 +42,13 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
41
42
|
|
|
42
43
|
// Limit to 1MB per item
|
|
43
44
|
if (sizeInKB > 1024) {
|
|
44
|
-
|
|
45
|
+
authLogger.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
|
|
45
46
|
return false;
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
return true;
|
|
49
50
|
} catch (error) {
|
|
50
|
-
|
|
51
|
+
authLogger.error(`Error checking data size for key "${key}":`, error);
|
|
51
52
|
return false;
|
|
52
53
|
}
|
|
53
54
|
};
|
|
@@ -72,7 +73,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
} catch (error) {
|
|
75
|
-
|
|
76
|
+
authLogger.error('Error clearing old localStorage data:', error);
|
|
76
77
|
}
|
|
77
78
|
};
|
|
78
79
|
|
|
@@ -88,7 +89,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
} catch (error) {
|
|
91
|
-
|
|
92
|
+
authLogger.error('Error force clearing localStorage:', error);
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
|
|
@@ -99,7 +100,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
99
100
|
|
|
100
101
|
// Check data size before attempting to save
|
|
101
102
|
if (!checkDataSize(valueToStore)) {
|
|
102
|
-
|
|
103
|
+
authLogger.warn(`Data size too large for key "${key}", removing key`);
|
|
103
104
|
// Remove the key if data is too large
|
|
104
105
|
try {
|
|
105
106
|
window.localStorage.removeItem(key);
|
|
@@ -127,10 +128,10 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
127
128
|
window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
128
129
|
} catch (storageError: any) {
|
|
129
130
|
// If quota exceeded, clear old data and try again
|
|
130
|
-
if (storageError.name === 'QuotaExceededError' ||
|
|
131
|
-
storageError.code === 22 ||
|
|
131
|
+
if (storageError.name === 'QuotaExceededError' ||
|
|
132
|
+
storageError.code === 22 ||
|
|
132
133
|
storageError.message?.includes('quota')) {
|
|
133
|
-
|
|
134
|
+
authLogger.warn('localStorage quota exceeded, clearing old data...');
|
|
134
135
|
clearOldData();
|
|
135
136
|
|
|
136
137
|
// Try again after clearing
|
|
@@ -143,7 +144,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
143
144
|
}
|
|
144
145
|
window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
145
146
|
} catch (retryError) {
|
|
146
|
-
|
|
147
|
+
authLogger.error(`Failed to set localStorage key "${key}" after clearing old data:`, retryError);
|
|
147
148
|
// If still fails, force clear all and try one more time
|
|
148
149
|
try {
|
|
149
150
|
forceClearAll();
|
|
@@ -155,7 +156,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
155
156
|
}
|
|
156
157
|
window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
157
158
|
} catch (finalError) {
|
|
158
|
-
|
|
159
|
+
authLogger.error(`Failed to set localStorage key "${key}" after force clearing:`, finalError);
|
|
159
160
|
// If still fails, just update the state without localStorage
|
|
160
161
|
setStoredValue(valueToStore);
|
|
161
162
|
}
|
|
@@ -166,7 +167,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
} catch (error) {
|
|
169
|
-
|
|
170
|
+
authLogger.error(`Error setting localStorage key "${key}":`, error);
|
|
170
171
|
// Still update the state even if localStorage fails
|
|
171
172
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
172
173
|
setStoredValue(valueToStore);
|
|
@@ -183,17 +184,17 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
183
184
|
window.localStorage.removeItem(`${key}_timestamp`);
|
|
184
185
|
} catch (removeError: any) {
|
|
185
186
|
// If removal fails due to quota, try to clear some data first
|
|
186
|
-
if (removeError.name === 'QuotaExceededError' ||
|
|
187
|
-
removeError.code === 22 ||
|
|
187
|
+
if (removeError.name === 'QuotaExceededError' ||
|
|
188
|
+
removeError.code === 22 ||
|
|
188
189
|
removeError.message?.includes('quota')) {
|
|
189
|
-
|
|
190
|
+
authLogger.warn('localStorage quota exceeded during removal, clearing old data...');
|
|
190
191
|
clearOldData();
|
|
191
192
|
|
|
192
193
|
try {
|
|
193
194
|
window.localStorage.removeItem(key);
|
|
194
195
|
window.localStorage.removeItem(`${key}_timestamp`);
|
|
195
196
|
} catch (retryError) {
|
|
196
|
-
|
|
197
|
+
authLogger.error(`Failed to remove localStorage key "${key}" after clearing:`, retryError);
|
|
197
198
|
// If still fails, force clear all
|
|
198
199
|
forceClearAll();
|
|
199
200
|
}
|
|
@@ -203,7 +204,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
} catch (error) {
|
|
206
|
-
|
|
207
|
+
authLogger.error(`Error removing localStorage key "${key}":`, error);
|
|
207
208
|
}
|
|
208
209
|
};
|
|
209
210
|
|
|
@@ -36,7 +36,10 @@ export function getCachedProfile(): User | null {
|
|
|
36
36
|
if (typeof window === 'undefined') return null;
|
|
37
37
|
|
|
38
38
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
39
|
-
if (!cached)
|
|
39
|
+
if (!cached) {
|
|
40
|
+
profileLogger.debug('No cached profile found');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
40
43
|
|
|
41
44
|
const cachedData: CachedProfile = JSON.parse(cached);
|
|
42
45
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
+
import { authLogger } from '../../utils/logger';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Simple sessionStorage hook with better error handling
|
|
@@ -17,7 +18,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
17
18
|
const item = window.sessionStorage.getItem(key);
|
|
18
19
|
return item ? JSON.parse(item) : initialValue;
|
|
19
20
|
} catch (error) {
|
|
20
|
-
|
|
21
|
+
authLogger.error(`Error reading sessionStorage key "${key}":`, error);
|
|
21
22
|
return initialValue;
|
|
22
23
|
}
|
|
23
24
|
});
|
|
@@ -31,13 +32,13 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
31
32
|
|
|
32
33
|
// Limit to 1MB per item
|
|
33
34
|
if (sizeInKB > 1024) {
|
|
34
|
-
|
|
35
|
+
authLogger.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
|
|
35
36
|
return false;
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
return true;
|
|
39
40
|
} catch (error) {
|
|
40
|
-
|
|
41
|
+
authLogger.error(`Error checking data size for key "${key}":`, error);
|
|
41
42
|
return false;
|
|
42
43
|
}
|
|
43
44
|
};
|
|
@@ -62,7 +63,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
} catch (error) {
|
|
65
|
-
|
|
66
|
+
authLogger.error('Error clearing old sessionStorage data:', error);
|
|
66
67
|
}
|
|
67
68
|
};
|
|
68
69
|
|
|
@@ -78,7 +79,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
} catch (error) {
|
|
81
|
-
|
|
82
|
+
authLogger.error('Error force clearing sessionStorage:', error);
|
|
82
83
|
}
|
|
83
84
|
};
|
|
84
85
|
|
|
@@ -89,7 +90,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
89
90
|
|
|
90
91
|
// Check data size before attempting to save
|
|
91
92
|
if (!checkDataSize(valueToStore)) {
|
|
92
|
-
|
|
93
|
+
authLogger.warn(`Data size too large for key "${key}", removing key`);
|
|
93
94
|
// Remove the key if data is too large
|
|
94
95
|
try {
|
|
95
96
|
window.sessionStorage.removeItem(key);
|
|
@@ -112,10 +113,10 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
112
113
|
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
113
114
|
} catch (storageError: any) {
|
|
114
115
|
// If quota exceeded, clear old data and try again
|
|
115
|
-
if (storageError.name === 'QuotaExceededError' ||
|
|
116
|
-
storageError.code === 22 ||
|
|
116
|
+
if (storageError.name === 'QuotaExceededError' ||
|
|
117
|
+
storageError.code === 22 ||
|
|
117
118
|
storageError.message?.includes('quota')) {
|
|
118
|
-
|
|
119
|
+
authLogger.warn('sessionStorage quota exceeded, clearing old data...');
|
|
119
120
|
clearOldData();
|
|
120
121
|
|
|
121
122
|
// Try again after clearing
|
|
@@ -123,14 +124,14 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
123
124
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
124
125
|
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
125
126
|
} catch (retryError) {
|
|
126
|
-
|
|
127
|
+
authLogger.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
|
|
127
128
|
// If still fails, force clear all and try one more time
|
|
128
129
|
try {
|
|
129
130
|
forceClearAll();
|
|
130
131
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
131
132
|
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
132
133
|
} catch (finalError) {
|
|
133
|
-
|
|
134
|
+
authLogger.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
|
|
134
135
|
// If still fails, just update the state without sessionStorage
|
|
135
136
|
setStoredValue(valueToStore);
|
|
136
137
|
}
|
|
@@ -141,7 +142,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
} catch (error) {
|
|
144
|
-
|
|
145
|
+
authLogger.error(`Error setting sessionStorage key "${key}":`, error);
|
|
145
146
|
// Still update the state even if sessionStorage fails
|
|
146
147
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
147
148
|
setStoredValue(valueToStore);
|
|
@@ -158,17 +159,17 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
158
159
|
window.sessionStorage.removeItem(`${key}_timestamp`);
|
|
159
160
|
} catch (removeError: any) {
|
|
160
161
|
// If removal fails due to quota, try to clear some data first
|
|
161
|
-
if (removeError.name === 'QuotaExceededError' ||
|
|
162
|
-
removeError.code === 22 ||
|
|
162
|
+
if (removeError.name === 'QuotaExceededError' ||
|
|
163
|
+
removeError.code === 22 ||
|
|
163
164
|
removeError.message?.includes('quota')) {
|
|
164
|
-
|
|
165
|
+
authLogger.warn('sessionStorage quota exceeded during removal, clearing old data...');
|
|
165
166
|
clearOldData();
|
|
166
167
|
|
|
167
168
|
try {
|
|
168
169
|
window.sessionStorage.removeItem(key);
|
|
169
170
|
window.sessionStorage.removeItem(`${key}_timestamp`);
|
|
170
171
|
} catch (retryError) {
|
|
171
|
-
|
|
172
|
+
authLogger.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
|
|
172
173
|
// If still fails, force clear all
|
|
173
174
|
forceClearAll();
|
|
174
175
|
}
|
|
@@ -178,7 +179,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
} catch (error) {
|
|
181
|
-
|
|
182
|
+
authLogger.error(`Error removing sessionStorage key "${key}":`, error);
|
|
182
183
|
}
|
|
183
184
|
};
|
|
184
185
|
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @djangocfg/layouts
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Reusable layout components and authentication system
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -13,3 +13,6 @@ export * from './layouts';
|
|
|
13
13
|
// Snippets - Reusable UI components
|
|
14
14
|
export * from './snippets';
|
|
15
15
|
|
|
16
|
+
// Validation error tracking
|
|
17
|
+
export * from './validation';
|
|
18
|
+
|
|
@@ -36,6 +36,7 @@ import { AdminLayout } from './layouts/AdminLayout';
|
|
|
36
36
|
import { determineLayoutMode, getRedirectUrl } from './utils';
|
|
37
37
|
import { useAuth } from '../../auth';
|
|
38
38
|
import type { AppLayoutConfig } from './types';
|
|
39
|
+
import type { ValidationErrorConfig } from '../../validation';
|
|
39
40
|
|
|
40
41
|
export interface AppLayoutProps {
|
|
41
42
|
children: ReactNode;
|
|
@@ -69,6 +70,12 @@ export interface AppLayoutProps {
|
|
|
69
70
|
* @example showPackageVersions={true}
|
|
70
71
|
*/
|
|
71
72
|
showPackageVersions?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Configuration for validation error tracking
|
|
75
|
+
* @default { enableToast: true, maxErrors: 50 }
|
|
76
|
+
* @example validationConfig={{ enableToast: false }}
|
|
77
|
+
*/
|
|
78
|
+
validationConfig?: Partial<ValidationErrorConfig>;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
/**
|
|
@@ -249,7 +256,7 @@ function LayoutRouter({
|
|
|
249
256
|
* </AppLayout>
|
|
250
257
|
* ```
|
|
251
258
|
*/
|
|
252
|
-
export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions }: AppLayoutProps) {
|
|
259
|
+
export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions, validationConfig }: AppLayoutProps) {
|
|
253
260
|
const router = useRouter();
|
|
254
261
|
|
|
255
262
|
// Check if ErrorBoundary is enabled (default: true)
|
|
@@ -292,7 +299,7 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
|
|
|
292
299
|
}} />
|
|
293
300
|
)}
|
|
294
301
|
|
|
295
|
-
<CoreProviders config={config}>
|
|
302
|
+
<CoreProviders config={config} validationConfig={validationConfig}>
|
|
296
303
|
{appContent}
|
|
297
304
|
</CoreProviders>
|
|
298
305
|
</>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import React, { Component, ReactNode } from 'react';
|
|
12
12
|
import { ErrorLayout } from '../../ErrorLayout';
|
|
13
13
|
import { Bug } from 'lucide-react';
|
|
14
|
+
import logger from '../../../utils/logger';
|
|
14
15
|
|
|
15
16
|
interface ErrorBoundaryProps {
|
|
16
17
|
children: ReactNode;
|
|
@@ -46,8 +47,8 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
46
47
|
|
|
47
48
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
48
49
|
// Log error
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
logger.error('ErrorBoundary caught error:', error);
|
|
51
|
+
logger.error('Error info:', errorInfo);
|
|
51
52
|
|
|
52
53
|
// Call optional callback
|
|
53
54
|
this.props.onError?.(error, errorInfo);
|