@jmruthers/pace-core 0.5.185 → 0.5.187

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 (300) hide show
  1. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  2. package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
  3. package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
  8. package/dist/chunk-3IC5WCMO.js.map +1 -0
  9. package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
  10. package/dist/chunk-3NFNJOO7.js.map +1 -0
  11. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  12. package/dist/chunk-63FOKYGO.js.map +1 -0
  13. package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
  14. package/dist/chunk-C4OYJOV4.js.map +1 -0
  15. package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
  19. package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/chunk-SAUPYVLF.js.map +1 -0
  23. package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
  26. package/dist/chunk-ULX5FYEM.js.map +1 -0
  27. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  28. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  29. package/dist/chunk-YHCN776L.js +447 -0
  30. package/dist/chunk-YHCN776L.js.map +1 -0
  31. package/dist/components.d.ts +4 -4
  32. package/dist/components.js +12 -10
  33. package/dist/components.js.map +1 -1
  34. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  35. package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
  36. package/dist/hooks.d.ts +265 -6
  37. package/dist/hooks.js +148 -49
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +25 -10
  40. package/dist/index.js +65 -30
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.js +1 -1
  43. package/dist/rbac/index.d.ts +125 -8
  44. package/dist/rbac/index.js +27 -7
  45. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  46. package/dist/types.d.ts +2 -2
  47. package/dist/types.js +1 -1
  48. package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
  49. package/dist/utils.d.ts +214 -4
  50. package/dist/utils.js +22 -2
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/classes/ColumnFactory.md +1 -1
  53. package/docs/api/classes/ErrorBoundary.md +1 -1
  54. package/docs/api/classes/InvalidScopeError.md +1 -1
  55. package/docs/api/classes/Logger.md +1 -1
  56. package/docs/api/classes/MissingUserContextError.md +1 -1
  57. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  58. package/docs/api/classes/PermissionDeniedError.md +1 -1
  59. package/docs/api/classes/RBACAuditManager.md +21 -17
  60. package/docs/api/classes/RBACCache.md +31 -23
  61. package/docs/api/classes/RBACEngine.md +6 -6
  62. package/docs/api/classes/RBACError.md +1 -1
  63. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  64. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  65. package/docs/api/classes/StorageUtils.md +1 -1
  66. package/docs/api/enums/FileCategory.md +1 -1
  67. package/docs/api/enums/LogLevel.md +1 -1
  68. package/docs/api/enums/RBACErrorCode.md +1 -1
  69. package/docs/api/enums/RPCFunction.md +1 -1
  70. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  71. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  72. package/docs/api/interfaces/AggregateConfig.md +1 -1
  73. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  74. package/docs/api/interfaces/BadgeProps.md +1 -1
  75. package/docs/api/interfaces/ButtonProps.md +1 -1
  76. package/docs/api/interfaces/CalendarProps.md +1 -1
  77. package/docs/api/interfaces/CardProps.md +1 -1
  78. package/docs/api/interfaces/ColorPalette.md +1 -1
  79. package/docs/api/interfaces/ColorShade.md +1 -1
  80. package/docs/api/interfaces/ComplianceResult.md +1 -1
  81. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  82. package/docs/api/interfaces/DataRecord.md +1 -1
  83. package/docs/api/interfaces/DataTableAction.md +1 -1
  84. package/docs/api/interfaces/DataTableColumn.md +1 -1
  85. package/docs/api/interfaces/DataTableProps.md +1 -1
  86. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  87. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  88. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  92. package/docs/api/interfaces/ExportColumn.md +1 -1
  93. package/docs/api/interfaces/ExportOptions.md +1 -1
  94. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  95. package/docs/api/interfaces/FileMetadata.md +1 -1
  96. package/docs/api/interfaces/FileReference.md +1 -1
  97. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  98. package/docs/api/interfaces/FileUploadOptions.md +33 -9
  99. package/docs/api/interfaces/FileUploadProps.md +36 -14
  100. package/docs/api/interfaces/FooterProps.md +1 -1
  101. package/docs/api/interfaces/FormFieldProps.md +1 -1
  102. package/docs/api/interfaces/FormProps.md +1 -1
  103. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  104. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  105. package/docs/api/interfaces/InputProps.md +1 -1
  106. package/docs/api/interfaces/LabelProps.md +1 -1
  107. package/docs/api/interfaces/LoggerConfig.md +1 -1
  108. package/docs/api/interfaces/LoginFormProps.md +1 -1
  109. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  110. package/docs/api/interfaces/NavigationContextType.md +1 -1
  111. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  112. package/docs/api/interfaces/NavigationItem.md +1 -1
  113. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  114. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  115. package/docs/api/interfaces/Organisation.md +1 -1
  116. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  117. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  118. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  119. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  120. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  121. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  122. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  123. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  124. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  125. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  126. package/docs/api/interfaces/PaletteData.md +1 -1
  127. package/docs/api/interfaces/ParsedAddress.md +120 -0
  128. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  129. package/docs/api/interfaces/ProgressProps.md +1 -1
  130. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  131. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  132. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  133. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  134. package/docs/api/interfaces/QuickFix.md +1 -1
  135. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  136. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  137. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  138. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  139. package/docs/api/interfaces/RBACConfig.md +27 -4
  140. package/docs/api/interfaces/RBACContext.md +1 -1
  141. package/docs/api/interfaces/RBACLogger.md +5 -5
  142. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  144. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  146. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  148. package/docs/api/interfaces/RBACResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  153. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  154. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  155. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  156. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  157. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  158. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  159. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  160. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  161. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  162. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  163. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  164. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  165. package/docs/api/interfaces/RouteConfig.md +1 -1
  166. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  167. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  168. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  169. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  170. package/docs/api/interfaces/SetupIssue.md +1 -1
  171. package/docs/api/interfaces/StorageConfig.md +1 -1
  172. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  173. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  174. package/docs/api/interfaces/StorageListOptions.md +1 -1
  175. package/docs/api/interfaces/StorageListResult.md +1 -1
  176. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  177. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  178. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  179. package/docs/api/interfaces/StyleImport.md +1 -1
  180. package/docs/api/interfaces/SwitchProps.md +1 -1
  181. package/docs/api/interfaces/TabsContentProps.md +1 -1
  182. package/docs/api/interfaces/TabsListProps.md +1 -1
  183. package/docs/api/interfaces/TabsProps.md +1 -1
  184. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  185. package/docs/api/interfaces/TextareaProps.md +1 -1
  186. package/docs/api/interfaces/ToastActionElement.md +1 -1
  187. package/docs/api/interfaces/ToastProps.md +1 -1
  188. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  189. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  190. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  191. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  192. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  193. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  195. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  199. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  201. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  202. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  203. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  204. package/docs/api/interfaces/UserEventAccess.md +1 -1
  205. package/docs/api/interfaces/UserMenuProps.md +1 -1
  206. package/docs/api/interfaces/UserProfile.md +1 -1
  207. package/docs/api/modules.md +328 -69
  208. package/docs/api-reference/components.md +26 -12
  209. package/docs/best-practices/performance.md +11 -0
  210. package/docs/implementation-guides/file-reference-system.md +24 -2
  211. package/docs/implementation-guides/file-upload-storage.md +38 -1
  212. package/docs/rbac/README.md +2 -1
  213. package/docs/rbac/api-reference.md +11 -0
  214. package/docs/rbac/performance.md +320 -0
  215. package/docs/standards/01-architecture-standard.md +5 -0
  216. package/docs/standards/05-security-standard.md +12 -0
  217. package/package.json +1 -1
  218. package/scripts/check-pace-core-compliance.js +512 -0
  219. package/src/components/AddressField/AddressField.test.tsx +411 -0
  220. package/src/components/AddressField/AddressField.tsx +323 -0
  221. package/src/components/AddressField/README.md +336 -0
  222. package/src/components/AddressField/index.ts +10 -0
  223. package/src/components/AddressField/types.ts +65 -0
  224. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  225. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  226. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  227. package/src/components/FileUpload/FileUpload.tsx +7 -1
  228. package/src/components/Header/Header.tsx +2 -5
  229. package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
  230. package/src/components/index.ts +2 -0
  231. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  232. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  233. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  234. package/src/hooks/index.ts +9 -0
  235. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  236. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  237. package/src/hooks/useAddressAutocomplete.ts +268 -0
  238. package/src/hooks/useFileDisplay.ts +3 -15
  239. package/src/hooks/useFileReference.test.ts +21 -3
  240. package/src/hooks/useFileReference.ts +3 -24
  241. package/src/hooks/useFileUrlCache.ts +246 -0
  242. package/src/hooks/useInactivityTracker.ts +31 -20
  243. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  244. package/src/hooks/useOrganisationSecurity.ts +3 -3
  245. package/src/hooks/usePreventTabReload.ts +106 -0
  246. package/src/hooks/useQueryCache.ts +315 -0
  247. package/src/hooks/useSecureDataAccess.ts +2 -2
  248. package/src/index.ts +2 -0
  249. package/src/providers/services/EventServiceProvider.tsx +4 -1
  250. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  251. package/src/rbac/api.test.ts +21 -6
  252. package/src/rbac/api.ts +32 -11
  253. package/src/rbac/audit-batched.ts +223 -0
  254. package/src/rbac/audit-enhanced.ts +2 -2
  255. package/src/rbac/audit.test.ts +6 -5
  256. package/src/rbac/audit.ts +34 -6
  257. package/src/rbac/cache-invalidation.ts +63 -12
  258. package/src/rbac/cache.test.ts +2 -2
  259. package/src/rbac/cache.ts +61 -14
  260. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  261. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  262. package/src/rbac/config.ts +9 -0
  263. package/src/rbac/engine.ts +2 -21
  264. package/src/rbac/hooks/usePermissions.ts +21 -5
  265. package/src/rbac/index.ts +19 -0
  266. package/src/rbac/performance.ts +210 -0
  267. package/src/rbac/request-deduplication.ts +87 -0
  268. package/src/rbac/utils/deep-equal.ts +93 -0
  269. package/src/styles/core.css +5 -5
  270. package/src/types/database.generated.ts +63 -9
  271. package/src/types/file-reference.ts +3 -1
  272. package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
  273. package/src/utils/file-reference/index.ts +56 -17
  274. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  275. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  276. package/src/utils/google-places/index.ts +26 -0
  277. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  278. package/src/utils/google-places/types.ts +94 -0
  279. package/src/utils/index.ts +23 -0
  280. package/src/utils/request-deduplication.ts +165 -0
  281. package/src/utils/security/secureDataAccess.ts +1 -1
  282. package/src/utils/storage/helpers.ts +211 -4
  283. package/dist/chunk-445GEP27.js.map +0 -1
  284. package/dist/chunk-AISXLWGZ.js.map +0 -1
  285. package/dist/chunk-FMUCXFII.js +0 -76
  286. package/dist/chunk-FMUCXFII.js.map +0 -1
  287. package/dist/chunk-FSFQFJCU.js.map +0 -1
  288. package/dist/chunk-FXFJRTKI.js.map +0 -1
  289. package/dist/chunk-HC67NW5K.js.map +0 -1
  290. package/dist/chunk-IXSNYUCT.js.map +0 -1
  291. package/dist/chunk-MX3EIJGQ.js.map +0 -1
  292. package/dist/chunk-OKI34GZD.js.map +0 -1
  293. package/dist/chunk-STTZQK2I.js.map +0 -1
  294. package/dist/chunk-U6WNSFX5.js.map +0 -1
  295. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  296. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  297. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  298. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  299. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  300. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
@@ -68,7 +68,7 @@
68
68
  * - LoadingSpinner - Loading state UI
69
69
  */
70
70
 
71
- import React, { useMemo } from 'react';
71
+ import React, { useMemo, useEffect, useRef, useState } from 'react';
72
72
  import { Navigate, Outlet } from 'react-router-dom';
73
73
  import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
74
74
  import { useSessionRestoration } from '../../hooks/useSessionRestoration';
@@ -77,6 +77,7 @@ import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner';
77
77
  import { SessionRestorationLoader } from '../SessionRestorationLoader';
78
78
  import { Alert, AlertDescription, AlertTitle } from '../Alert/Alert';
79
79
  import { logger } from '../../utils/core/logger';
80
+ import { usePreventTabReload } from '../../hooks/usePreventTabReload';
80
81
 
81
82
  export interface ProtectedRouteProps {
82
83
  /**
@@ -148,6 +149,96 @@ export function ProtectedRoute({
148
149
 
149
150
  const sessionRestoration = useSessionRestoration();
150
151
 
152
+ // Prevent full page reloads when switching tabs (handles bfcache and visibility changes)
153
+ usePreventTabReload({ enabled: true, gracePeriodMs: 2000 });
154
+
155
+ // Track if user was previously authenticated to prevent redirects during session refresh
156
+ const wasAuthenticatedRef = useRef(false);
157
+ const [shouldRedirect, setShouldRedirect] = useState(false);
158
+ const tabJustBecameVisibleRef = useRef(false);
159
+
160
+ // Track authentication state to detect when user was previously logged in
161
+ useEffect(() => {
162
+ if (isAuthenticated) {
163
+ wasAuthenticatedRef.current = true;
164
+ setShouldRedirect(false);
165
+ tabJustBecameVisibleRef.current = false; // Clear visibility flag when authenticated
166
+ }
167
+ }, [isAuthenticated]);
168
+
169
+ // Handle tab visibility changes - prevent immediate redirects when tab becomes visible
170
+ // This prevents the page from refreshing when switching back to the tab
171
+ useEffect(() => {
172
+ if (typeof document === 'undefined') return;
173
+
174
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
175
+ let wasHidden = document.hidden;
176
+
177
+ const handleVisibilityChange = () => {
178
+ const isNowVisible = !document.hidden;
179
+
180
+ // When tab becomes visible, immediately prevent redirects and give session refresh time
181
+ if (isNowVisible && wasHidden) {
182
+ // Tab just became visible - immediately prevent redirects
183
+ if (!isAuthenticated && wasAuthenticatedRef.current) {
184
+ tabJustBecameVisibleRef.current = true;
185
+ setShouldRedirect(false); // Immediately clear redirect flag
186
+
187
+ // Clear any existing timeout
188
+ if (timeoutId) {
189
+ clearTimeout(timeoutId);
190
+ }
191
+
192
+ // Wait a bit to see if session refresh completes
193
+ timeoutId = setTimeout(() => {
194
+ // Only allow redirect if still not authenticated after delay
195
+ tabJustBecameVisibleRef.current = false;
196
+ // Use a function to get the latest state
197
+ setShouldRedirect((prev) => {
198
+ // Only set to true if we're still not authenticated
199
+ // This will be checked again in the render logic
200
+ return prev;
201
+ });
202
+ }, 2000); // 2 second grace period for session refresh
203
+ }
204
+ } else if (!isNowVisible) {
205
+ // Tab became hidden - clear the visibility flag
206
+ tabJustBecameVisibleRef.current = false;
207
+ if (timeoutId) {
208
+ clearTimeout(timeoutId);
209
+ timeoutId = null;
210
+ }
211
+ }
212
+
213
+ wasHidden = !isNowVisible;
214
+ };
215
+
216
+ // Check initial state - if tab is visible and user appears logged out, give grace period
217
+ if (!document.hidden && !isAuthenticated && wasAuthenticatedRef.current) {
218
+ tabJustBecameVisibleRef.current = true;
219
+ setShouldRedirect(false);
220
+ timeoutId = setTimeout(() => {
221
+ tabJustBecameVisibleRef.current = false;
222
+ }, 2000);
223
+ }
224
+
225
+ document.addEventListener('visibilitychange', handleVisibilityChange);
226
+ return () => {
227
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
228
+ if (timeoutId) {
229
+ clearTimeout(timeoutId);
230
+ }
231
+ };
232
+ }, [isAuthenticated]);
233
+
234
+ // Reset redirect flag when authenticated
235
+ useEffect(() => {
236
+ if (isAuthenticated) {
237
+ setShouldRedirect(false);
238
+ tabJustBecameVisibleRef.current = false;
239
+ }
240
+ }, [isAuthenticated]);
241
+
151
242
  const isRestoringSession = useMemo(() => {
152
243
  return sessionRestoration.isRestoring &&
153
244
  !sessionRestoration.restorationComplete &&
@@ -182,13 +273,55 @@ export function ProtectedRoute({
182
273
  }
183
274
 
184
275
  // Redirect to login if not authenticated
276
+ // Priority order:
277
+ // 1. If session restoration has timed out or errored → redirect immediately (even if loading)
278
+ // 2. If user was never authenticated → redirect immediately (even if loading)
279
+ // 3. If tab just became visible → show loading (prevent redirect during grace period)
280
+ // 4. If we've confirmed they should redirect (after visibility change grace period) → redirect
281
+ // 5. Otherwise, if loading → show loading spinner (session might be refreshing)
282
+ // 6. Otherwise → redirect (user is not authenticated and not loading)
185
283
  if (!isAuthenticated) {
284
+ // Session restoration timeout/error always redirects immediately
186
285
  if (sessionRestoration.hasTimedOut || sessionRestoration.restorationError) {
187
286
  logger.warn('ProtectedRoute', 'Session restoration failed, redirecting to login', {
188
287
  timedOut: sessionRestoration.hasTimedOut,
189
288
  error: sessionRestoration.restorationError?.message
190
289
  });
290
+ return <Navigate to={loginPath} replace />;
291
+ }
292
+
293
+ // User was never authenticated → redirect immediately
294
+ if (!wasAuthenticatedRef.current) {
295
+ return <Navigate to={loginPath} replace />;
296
+ }
297
+
298
+ // Tab just became visible - show loading to prevent redirect during grace period
299
+ // Also check document visibility state directly as a fallback
300
+ const isTabVisible = typeof document !== 'undefined' && !document.hidden;
301
+ if (tabJustBecameVisibleRef.current || (isTabVisible && wasAuthenticatedRef.current && isLoading)) {
302
+ return loadingFallback || (
303
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
304
+ <LoadingSpinner />
305
+ </div>
306
+ );
191
307
  }
308
+
309
+ // We've confirmed redirect after grace period → redirect
310
+ if (shouldRedirect) {
311
+ return <Navigate to={loginPath} replace />;
312
+ }
313
+
314
+ // User was authenticated before but now appears logged out
315
+ // Show loading state while we wait for session refresh (unless we're not loading)
316
+ if (isLoading) {
317
+ return loadingFallback || (
318
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
319
+ <LoadingSpinner />
320
+ </div>
321
+ );
322
+ }
323
+
324
+ // Not loading and not authenticated → redirect
192
325
  return <Navigate to={loginPath} replace />;
193
326
  }
194
327
 
@@ -43,6 +43,8 @@ export type { CardProps, CardActionsProps } from './Card';
43
43
 
44
44
  export { Input } from './Input';
45
45
  export type { InputProps } from './Input';
46
+ export { AddressField } from './AddressField';
47
+ export type { AddressFieldProps, AddressFieldRef, ParsedAddress, AutocompleteOptions } from './AddressField';
46
48
  export { Label } from './Label';
47
49
  export type { LabelProps } from './Label';
48
50
 
@@ -25,7 +25,18 @@ import { FileCategory as FileCategoryEnum } from '../../types/file-reference';
25
25
  // Mock storage helpers
26
26
  vi.mock('../../utils/storage/helpers', () => ({
27
27
  getPublicUrl: vi.fn((supabase: any, path: string) => `https://example.com/${path}`),
28
- getSignedUrl: vi.fn().mockResolvedValue({ url: 'https://example.com/signed-file.jpg', expiresAt: new Date().toISOString() })
28
+ getSignedUrl: vi.fn().mockResolvedValue({ url: 'https://example.com/signed-file.jpg', expiresAt: new Date().toISOString() }),
29
+ generateFileUrlsBatch: vi.fn().mockImplementation(async (supabase, files) => {
30
+ const urlMap = new Map<string, string>();
31
+ for (const file of files) {
32
+ if (file.is_public) {
33
+ urlMap.set(file.id, `https://example.com/${file.file_path}`);
34
+ } else {
35
+ urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
36
+ }
37
+ }
38
+ return urlMap;
39
+ })
29
40
  }));
30
41
 
31
42
  // Mock file reference service
@@ -38,7 +49,7 @@ vi.mock('../../utils/file-reference', () => ({
38
49
  createFileReferenceService: vi.fn(() => mockService)
39
50
  }));
40
51
 
41
- import { getPublicUrl, getSignedUrl } from '../../utils/storage/helpers';
52
+ import { getPublicUrl, getSignedUrl, generateFileUrlsBatch } from '../../utils/storage/helpers';
42
53
  import { createFileReferenceService } from '../../utils/file-reference';
43
54
 
44
55
  describe('useFileDisplay Hook', () => {
@@ -87,6 +98,19 @@ describe('useFileDisplay Hook', () => {
87
98
  mockSupabase = createMockSupabaseClient() as any;
88
99
  mockService.getFilesByCategory.mockResolvedValue([]);
89
100
  mockService.listFileReferences.mockResolvedValue([]);
101
+
102
+ // Reset generateFileUrlsBatch mock to ensure it returns a Map
103
+ vi.mocked(generateFileUrlsBatch).mockImplementation(async (supabase, files) => {
104
+ const urlMap = new Map<string, string>();
105
+ for (const file of files) {
106
+ if (file.is_public) {
107
+ urlMap.set(file.id, `https://example.com/${file.file_path}`);
108
+ } else {
109
+ urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
110
+ }
111
+ }
112
+ return urlMap;
113
+ });
90
114
  });
91
115
 
92
116
  afterEach(() => {
@@ -305,13 +329,14 @@ describe('useFileDisplay Hook', () => {
305
329
  await waitFor(
306
330
  () => {
307
331
  expect(result.current.isLoading).toBe(false);
332
+ expect(result.current.fileCount).toBe(2);
333
+ expect(result.current.fileReferences.length).toBe(2);
334
+ expect(result.current.fileUrls).toBeDefined();
335
+ expect(result.current.fileUrls.size).toBe(2);
308
336
  },
309
337
  { timeout: 2000 }
310
338
  );
311
339
 
312
- expect(result.current.fileCount).toBe(2);
313
- expect(result.current.fileReferences.length).toBe(2);
314
- expect(result.current.fileUrls.size).toBe(2);
315
340
  expect(result.current.fileUrl).toBe(null); // No single file URL in multiple mode
316
341
  expect(result.current.fileReference).toBe(null); // No single file reference in multiple mode
317
342
  });
@@ -19,6 +19,7 @@ vi.mock('../../hooks/useOrganisations', () => ({
19
19
  // Mock the RBAC API
20
20
  vi.mock('../../rbac/api', () => ({
21
21
  isPermitted: vi.fn(),
22
+ isPermittedCached: vi.fn(),
22
23
  getPermissionMap: vi.fn(),
23
24
  emitAuditEvent: vi.fn()
24
25
  }));
@@ -451,8 +452,8 @@ describe('useOrganisationSecurity', () => {
451
452
  const mockOrg = { id: 'org-123' };
452
453
 
453
454
  // Mock the RBAC API
454
- const { isPermitted } = await import('../../rbac/api');
455
- vi.mocked(isPermitted).mockResolvedValue(true);
455
+ const { isPermittedCached } = await import('../../rbac/api');
456
+ vi.mocked(isPermittedCached).mockResolvedValue(true);
456
457
 
457
458
  vi.mocked(useUnifiedAuth).mockReturnValue({
458
459
  ...mockUseUnifiedAuth,
@@ -469,7 +470,7 @@ describe('useOrganisationSecurity', () => {
469
470
  const hasPermission = await result.current.hasPermission('view_basic');
470
471
 
471
472
  expect(hasPermission).toBe(true);
472
- expect(isPermitted).toHaveBeenCalledWith({
473
+ expect(isPermittedCached).toHaveBeenCalledWith({
473
474
  userId: 'user-123',
474
475
  scope: {
475
476
  organisationId: 'org-123',
@@ -485,8 +486,8 @@ describe('useOrganisationSecurity', () => {
485
486
  const mockOrg = { id: 'org-123' };
486
487
 
487
488
  // Mock the RBAC API to throw an error
488
- const { isPermitted } = await import('../../rbac/api');
489
- vi.mocked(isPermitted).mockRejectedValue(new Error('RBAC API error'));
489
+ const { isPermittedCached } = await import('../../rbac/api');
490
+ vi.mocked(isPermittedCached).mockRejectedValue(new Error('RBAC API error'));
490
491
 
491
492
  vi.mocked(useUnifiedAuth).mockReturnValue({
492
493
  ...mockUseUnifiedAuth,
@@ -510,8 +511,8 @@ describe('useOrganisationSecurity', () => {
510
511
  const mockOrg = { id: 'org-123' };
511
512
 
512
513
  // Mock the RBAC API to throw an error
513
- const { isPermitted } = await import('../../rbac/api');
514
- vi.mocked(isPermitted).mockImplementation(() => {
514
+ const { isPermittedCached } = await import('../../rbac/api');
515
+ vi.mocked(isPermittedCached).mockImplementation(() => {
515
516
  throw new Error('RBAC API exception');
516
517
  });
517
518
 
@@ -536,8 +537,8 @@ describe('useOrganisationSecurity', () => {
536
537
  const mockUser = { id: 'user-123', user_metadata: { eventId: 'event-123', appId: 'app-123' } };
537
538
 
538
539
  // Mock the RBAC API
539
- const { isPermitted } = await import('../../rbac/api');
540
- vi.mocked(isPermitted).mockResolvedValue(true);
540
+ const { isPermittedCached } = await import('../../rbac/api');
541
+ vi.mocked(isPermittedCached).mockResolvedValue(true);
541
542
 
542
543
  vi.mocked(useUnifiedAuth).mockReturnValue({
543
544
  ...mockUseUnifiedAuth,
@@ -549,7 +550,7 @@ describe('useOrganisationSecurity', () => {
549
550
 
550
551
  await result.current.hasPermission('view_basic', 'org-456');
551
552
 
552
- expect(isPermitted).toHaveBeenCalledWith({
553
+ expect(isPermittedCached).toHaveBeenCalledWith({
553
554
  userId: 'user-123',
554
555
  scope: {
555
556
  organisationId: 'org-456',
@@ -21,9 +21,20 @@ import type { Database } from '../../types/database';
21
21
  import type { FileCategory } from '../../types/file-reference';
22
22
  import { FileCategory as FileCategoryEnum } from '../../types/file-reference';
23
23
 
24
- // Mock getPublicUrl
24
+ // Mock getPublicUrl and generateFileUrlsBatch
25
25
  vi.mock('../../utils/storage/helpers', () => ({
26
- getPublicUrl: vi.fn((supabase: any, path: string) => `https://example.com/${path}`)
26
+ getPublicUrl: vi.fn((supabase: any, path: string) => `https://example.com/${path}`),
27
+ generateFileUrlsBatch: vi.fn().mockImplementation(async (supabase, files) => {
28
+ const urlMap = new Map<string, string>();
29
+ for (const file of files) {
30
+ if (file.is_public) {
31
+ urlMap.set(file.id, `https://example.com/${file.file_path}`);
32
+ } else {
33
+ urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
34
+ }
35
+ }
36
+ return urlMap;
37
+ })
27
38
  }));
28
39
 
29
40
  // Mock logger
@@ -36,7 +47,7 @@ vi.mock('../../utils/core/logger', () => ({
36
47
  },
37
48
  }));
38
49
 
39
- import { getPublicUrl } from '../../utils/storage/helpers';
50
+ import { getPublicUrl, generateFileUrlsBatch } from '../../utils/storage/helpers';
40
51
 
41
52
  describe('usePublicFileDisplay Hook', () => {
42
53
  let mockSupabase: SupabaseClient<Database>;
@@ -61,6 +72,19 @@ describe('usePublicFileDisplay Hook', () => {
61
72
  vi.clearAllMocks();
62
73
  clearPublicFileDisplayCache();
63
74
  mockSupabase = createMockSupabaseClient() as any;
75
+
76
+ // Reset generateFileUrlsBatch mock to ensure it returns a Map
77
+ vi.mocked(generateFileUrlsBatch).mockImplementation(async (supabase, files) => {
78
+ const urlMap = new Map<string, string>();
79
+ for (const file of files) {
80
+ if (file.is_public) {
81
+ urlMap.set(file.id, `https://example.com/${file.file_path}`);
82
+ } else {
83
+ urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
84
+ }
85
+ }
86
+ return urlMap;
87
+ });
64
88
  });
65
89
 
66
90
  afterEach(() => {
@@ -256,13 +280,14 @@ describe('usePublicFileDisplay Hook', () => {
256
280
  await waitFor(
257
281
  () => {
258
282
  expect(result.current.isLoading).toBe(false);
283
+ expect(result.current.fileCount).toBe(2);
284
+ expect(result.current.fileReferences.length).toBe(2);
285
+ expect(result.current.fileUrls).toBeDefined();
286
+ expect(result.current.fileUrls.size).toBe(2);
259
287
  },
260
288
  { timeout: 2000 }
261
289
  );
262
290
 
263
- expect(result.current.fileCount).toBe(2);
264
- expect(result.current.fileReferences.length).toBe(2);
265
- expect(result.current.fileUrls.size).toBe(2);
266
291
  expect(result.current.fileUrls.get('file-123')).toBe('https://example.com/org-123/logos/logo.png');
267
292
  expect(result.current.fileUrls.get('file-456')).toBe('https://example.com/org-123/logos/logo2.png');
268
293
  expect(result.current.fileUrl).toBe(null); // No single file URL in multiple mode
@@ -28,6 +28,8 @@ export { useEventTheme } from './useEventTheme';
28
28
  // === DATA & STATE HOOKS ===
29
29
  export { useDebounce } from './useDebounce';
30
30
  export { useDataTableState } from './useDataTableState';
31
+ export { useAddressAutocomplete } from './useAddressAutocomplete';
32
+ export type { UseAddressAutocompleteOptions, UseAddressAutocompleteReturn } from './useAddressAutocomplete';
31
33
 
32
34
  // === ORGANISATION HOOKS ===
33
35
  export { useOrganisationPermissions } from './useOrganisationPermissions';
@@ -48,6 +50,8 @@ export { useComponentPerformance } from './useComponentPerformance';
48
50
  export { useAppConfig } from './useAppConfig';
49
51
  export type { UseAppConfigReturn } from './useAppConfig';
50
52
  export { usePerformanceMonitor } from './usePerformanceMonitor';
53
+ export { useQueryCache, queryCacheHelpers } from './useQueryCache';
54
+ export type { UseQueryCacheReturn, UseQueryCacheOptions } from './useQueryCache';
51
55
 
52
56
  // DataTable performance hook
53
57
  export { useDataTablePerformance } from './useDataTablePerformance';
@@ -56,6 +60,8 @@ export type { UseDataTablePerformanceOptions, UseDataTablePerformanceReturn } fr
56
60
  // === FILE DISPLAY HOOKS ===
57
61
  export { useFileDisplay, clearFileDisplayCache, getFileDisplayCacheStats, invalidateFileDisplayCache } from './useFileDisplay';
58
62
  export type { UseFileDisplayReturn, UseFileDisplayOptions } from './useFileDisplay';
63
+ export { useFileUrlCache } from './useFileUrlCache';
64
+ export type { UseFileUrlCacheReturn } from './useFileUrlCache';
59
65
 
60
66
  // === STORAGE HOOKS ===
61
67
  export { useStorage, useFileUpload } from './useStorage';
@@ -64,6 +70,9 @@ export type { UseStorageOptions, UseStorageReturn } from './useStorage';
64
70
  // === PUBLIC DATA ACCESS HOOKS ===
65
71
  export * from './public';
66
72
 
73
+ // === PAGE LIFECYCLE HOOKS ===
74
+ export { usePreventTabReload } from './usePreventTabReload';
75
+ export type { UsePreventTabReloadOptions } from './usePreventTabReload';
67
76
 
68
77
  // RBAC Hooks - Use @jmruthers/pace-core/rbac instead
69
78
  // Note: RBAC functionality has been moved to the dedicated RBAC module
@@ -40,7 +40,7 @@ import { useState, useEffect, useCallback } from 'react';
40
40
  import type { SupabaseClient } from '@supabase/supabase-js';
41
41
  import type { Database } from '../../types/database';
42
42
  import { FileReference, FileCategory } from '../../types/file-reference';
43
- import { getPublicUrl } from '../../utils/storage/helpers';
43
+ import { getPublicUrl, generateFileUrlsBatch } from '../../utils/storage/helpers';
44
44
  import { logger } from '../../utils/core/logger';
45
45
 
46
46
  // Simple in-memory cache for public file data
@@ -236,7 +236,7 @@ export function usePublicFileDisplay(
236
236
  const ids = fileIds.map((item: any) => item.id);
237
237
  const { data: fullData, error: fetchError } = await supabase
238
238
  .from('file_references')
239
- .select('*')
239
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
240
240
  .in('id', ids)
241
241
  .eq('is_public', true); // Only public files in public context
242
242
 
@@ -295,14 +295,12 @@ export function usePublicFileDisplay(
295
295
  const url = getPublicUrl(supabase, firstFile.file_path, true);
296
296
  setFileUrl(url);
297
297
  } else {
298
- // Multiple files mode - generate URLs for all files
299
- const urlMap = new Map<string, string>();
300
- for (const fileRef of fileRefs) {
301
- const url = getPublicUrl(supabase, fileRef.file_path, true);
302
- if (url) {
303
- urlMap.set(fileRef.id, url);
304
- }
305
- }
298
+ // Multiple files mode - generate URLs for all files in batch
299
+ const urlMap = await generateFileUrlsBatch(supabase, fileRefs, {
300
+ appName: 'pace-core',
301
+ orgId: organisation_id,
302
+ expiresIn: 3600
303
+ });
306
304
  setFileUrls(urlMap);
307
305
  setFileReference(null);
308
306
  setFileUrl(null);