@jmruthers/pace-core 0.5.184 → 0.5.186

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 (319) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +60 -1
  3. package/core-usage-manifest.json +312 -0
  4. package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
  5. package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
  6. package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +23 -10
  7. package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
  8. package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
  9. package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
  10. package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
  11. package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
  12. package/dist/{chunk-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
  13. package/dist/chunk-DAGICKHT.js.map +1 -0
  14. package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
  15. package/dist/chunk-FXFJRTKI.js.map +1 -0
  16. package/dist/{chunk-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
  17. package/dist/chunk-GRIQLQ52.js.map +1 -0
  18. package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
  19. package/dist/chunk-HDCUMOOI.js.map +1 -0
  20. package/dist/chunk-HESYZWZW.js +388 -0
  21. package/dist/chunk-HESYZWZW.js.map +1 -0
  22. package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
  23. package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
  24. package/dist/{chunk-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
  25. package/dist/chunk-OALXJH4Y.js.map +1 -0
  26. package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
  27. package/dist/chunk-TC7D3CR3.js.map +1 -0
  28. package/dist/chunk-THRPYOFK.js +215 -0
  29. package/dist/chunk-THRPYOFK.js.map +1 -0
  30. package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
  31. package/dist/chunk-U6WNSFX5.js.map +1 -0
  32. package/dist/{chunk-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
  33. package/dist/chunk-UQWSHFVX.js.map +1 -0
  34. package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
  35. package/dist/components.d.ts +182 -6
  36. package/dist/components.js +157 -11
  37. package/dist/components.js.map +1 -1
  38. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  39. package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
  40. package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
  41. package/dist/hooks.d.ts +52 -15
  42. package/dist/hooks.js +12 -22
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +12 -12
  45. package/dist/index.js +82 -18
  46. package/dist/index.js.map +1 -1
  47. package/dist/providers.d.ts +1 -1
  48. package/dist/providers.js +3 -1
  49. package/dist/rbac/index.d.ts +206 -15
  50. package/dist/rbac/index.js +28 -6
  51. package/dist/timezone-_pgH8qrY.d.ts +530 -0
  52. package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
  53. package/dist/types.d.ts +2 -2
  54. package/dist/types.js +1 -1
  55. package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
  56. package/dist/utils.d.ts +110 -152
  57. package/dist/utils.js +128 -138
  58. package/dist/utils.js.map +1 -1
  59. package/docs/api/README.md +60 -1
  60. package/docs/api/classes/ColumnFactory.md +1 -1
  61. package/docs/api/classes/ErrorBoundary.md +1 -1
  62. package/docs/api/classes/InvalidScopeError.md +1 -1
  63. package/docs/api/classes/Logger.md +178 -0
  64. package/docs/api/classes/MissingUserContextError.md +1 -1
  65. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  66. package/docs/api/classes/PermissionDeniedError.md +1 -1
  67. package/docs/api/classes/RBACAuditManager.md +2 -2
  68. package/docs/api/classes/RBACCache.md +1 -1
  69. package/docs/api/classes/RBACEngine.md +2 -2
  70. package/docs/api/classes/RBACError.md +1 -1
  71. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  72. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  73. package/docs/api/classes/StorageUtils.md +1 -1
  74. package/docs/api/enums/FileCategory.md +1 -1
  75. package/docs/api/enums/LogLevel.md +54 -0
  76. package/docs/api/enums/RBACErrorCode.md +1 -1
  77. package/docs/api/enums/RPCFunction.md +1 -1
  78. package/docs/api/interfaces/AggregateConfig.md +1 -1
  79. package/docs/api/interfaces/BadgeProps.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +1 -1
  81. package/docs/api/interfaces/CalendarProps.md +18 -2
  82. package/docs/api/interfaces/CardProps.md +1 -1
  83. package/docs/api/interfaces/ColorPalette.md +1 -1
  84. package/docs/api/interfaces/ColorShade.md +1 -1
  85. package/docs/api/interfaces/ComplianceResult.md +30 -0
  86. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  87. package/docs/api/interfaces/DataRecord.md +1 -1
  88. package/docs/api/interfaces/DataTableAction.md +1 -1
  89. package/docs/api/interfaces/DataTableColumn.md +1 -1
  90. package/docs/api/interfaces/DataTableProps.md +1 -1
  91. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  92. package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
  93. package/docs/api/interfaces/DatabaseIssue.md +41 -0
  94. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  95. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/EventAppRoleData.md +6 -6
  97. package/docs/api/interfaces/ExportColumn.md +1 -1
  98. package/docs/api/interfaces/ExportOptions.md +1 -1
  99. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  100. package/docs/api/interfaces/FileMetadata.md +1 -1
  101. package/docs/api/interfaces/FileReference.md +1 -1
  102. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  103. package/docs/api/interfaces/FileUploadOptions.md +48 -8
  104. package/docs/api/interfaces/FileUploadProps.md +46 -13
  105. package/docs/api/interfaces/FooterProps.md +1 -1
  106. package/docs/api/interfaces/FormFieldProps.md +1 -1
  107. package/docs/api/interfaces/FormProps.md +1 -1
  108. package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
  109. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  110. package/docs/api/interfaces/InputProps.md +1 -1
  111. package/docs/api/interfaces/LabelProps.md +1 -1
  112. package/docs/api/interfaces/LoggerConfig.md +62 -0
  113. package/docs/api/interfaces/LoginFormProps.md +1 -1
  114. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  115. package/docs/api/interfaces/NavigationContextType.md +1 -1
  116. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  117. package/docs/api/interfaces/NavigationItem.md +1 -1
  118. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  119. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  120. package/docs/api/interfaces/Organisation.md +1 -1
  121. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  122. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  123. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  124. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  125. package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
  126. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  127. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  128. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  129. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  130. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  131. package/docs/api/interfaces/PaletteData.md +1 -1
  132. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  133. package/docs/api/interfaces/ProgressProps.md +1 -1
  134. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  135. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  136. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  138. package/docs/api/interfaces/QuickFix.md +52 -0
  139. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  140. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  141. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  142. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  143. package/docs/api/interfaces/RBACConfig.md +4 -4
  144. package/docs/api/interfaces/RBACContext.md +1 -1
  145. package/docs/api/interfaces/RBACLogger.md +1 -1
  146. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  148. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  149. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  151. package/docs/api/interfaces/RBACResult.md +1 -1
  152. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  153. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  154. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  155. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  156. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  157. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  158. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  159. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  160. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  161. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  162. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  163. package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
  164. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  165. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  166. package/docs/api/interfaces/RoleManagementResult.md +5 -5
  167. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  168. package/docs/api/interfaces/RouteConfig.md +1 -1
  169. package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
  170. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  171. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  172. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  173. package/docs/api/interfaces/SetupIssue.md +41 -0
  174. package/docs/api/interfaces/StorageConfig.md +1 -1
  175. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  176. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  177. package/docs/api/interfaces/StorageListOptions.md +1 -1
  178. package/docs/api/interfaces/StorageListResult.md +1 -1
  179. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  180. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  181. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  182. package/docs/api/interfaces/StyleImport.md +1 -1
  183. package/docs/api/interfaces/SwitchProps.md +1 -1
  184. package/docs/api/interfaces/TabsContentProps.md +1 -1
  185. package/docs/api/interfaces/TabsListProps.md +1 -1
  186. package/docs/api/interfaces/TabsProps.md +1 -1
  187. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  188. package/docs/api/interfaces/TextareaProps.md +1 -1
  189. package/docs/api/interfaces/ToastActionElement.md +1 -1
  190. package/docs/api/interfaces/ToastProps.md +1 -1
  191. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  192. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  193. package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
  194. package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
  195. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  196. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  198. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  199. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  201. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  202. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  203. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  204. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  205. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  206. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  207. package/docs/api/interfaces/UserEventAccess.md +1 -1
  208. package/docs/api/interfaces/UserMenuProps.md +1 -1
  209. package/docs/api/interfaces/UserProfile.md +1 -1
  210. package/docs/api/modules.md +746 -50
  211. package/docs/api-reference/components.md +26 -12
  212. package/docs/api-reference/hooks.md +111 -0
  213. package/docs/api-reference/rpc-functions.md +1 -1
  214. package/docs/api-reference/utilities.md +184 -0
  215. package/docs/getting-started/installation-guide.md +75 -16
  216. package/docs/getting-started/quick-start.md +61 -11
  217. package/docs/implementation-guides/authentication.md +88 -12
  218. package/docs/implementation-guides/file-reference-system.md +26 -3
  219. package/docs/implementation-guides/file-upload-storage.md +30 -1
  220. package/docs/rbac/README.md +1 -0
  221. package/docs/rbac/compliance/compliance-guide.md +544 -0
  222. package/docs/rbac/getting-started.md +158 -33
  223. package/docs/standards/pace-core-compliance.md +432 -0
  224. package/eslint-config-pace-core.cjs +93 -0
  225. package/package.json +15 -3
  226. package/scripts/analyze-bundle.js +232 -0
  227. package/scripts/build-css.js +56 -0
  228. package/scripts/build-docs-incremental.js +1015 -0
  229. package/scripts/check-pace-core-compliance.cjs +2353 -0
  230. package/scripts/check-pace-core-compliance.js +512 -0
  231. package/scripts/generate-docs.js +157 -0
  232. package/scripts/setup-build-cache.js +73 -0
  233. package/scripts/utils/command-runner.js +131 -0
  234. package/scripts/utils/env.js +33 -0
  235. package/scripts/utils/index.js +10 -0
  236. package/scripts/utils/logger.js +88 -0
  237. package/scripts/utils/path-helpers.js +37 -0
  238. package/scripts/validate-formats.js +133 -0
  239. package/scripts/validate-master.js +155 -0
  240. package/scripts/validate-pre-publish.js +140 -0
  241. package/scripts/validate-theme.js +142 -0
  242. package/src/components/Calendar/Calendar.tsx +8 -1
  243. package/src/components/Card/Card.tsx +47 -8
  244. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
  245. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
  246. package/src/components/DatePickerWithTimezone/README.md +135 -0
  247. package/src/components/DatePickerWithTimezone/index.ts +10 -0
  248. package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
  249. package/src/components/DateTimeField/DateTimeField.tsx +232 -0
  250. package/src/components/DateTimeField/README.md +148 -0
  251. package/src/components/DateTimeField/index.ts +10 -0
  252. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  253. package/src/components/FileUpload/FileUpload.tsx +10 -1
  254. package/src/components/Header/Header.test.tsx +47 -18
  255. package/src/components/Header/Header.tsx +22 -7
  256. package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
  257. package/src/components/PaceAppLayout/README.md +9 -0
  258. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
  259. package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
  260. package/src/components/index.ts +8 -0
  261. package/src/eslint-rules/pace-core-compliance.cjs +406 -0
  262. package/src/eslint-rules/pace-core-compliance.js +640 -0
  263. package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
  264. package/src/hooks/index.ts +5 -0
  265. package/src/hooks/useFileReference.test.ts +2 -0
  266. package/src/hooks/useFormDialog.ts +147 -0
  267. package/src/hooks/usePreventTabReload.ts +106 -0
  268. package/src/hooks/useSecureDataAccess.ts +2 -2
  269. package/src/index.ts +27 -0
  270. package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
  271. package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
  272. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  273. package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
  274. package/src/rbac/compliance/database-validator.ts +165 -0
  275. package/src/rbac/compliance/index.ts +38 -0
  276. package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
  277. package/src/rbac/compliance/runtime-compliance.ts +77 -0
  278. package/src/rbac/compliance/setup-validator.ts +131 -0
  279. package/src/rbac/components/PagePermissionGuard.tsx +8 -64
  280. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
  281. package/src/rbac/docs/event-based-apps.md +285 -0
  282. package/src/rbac/errors.ts +11 -0
  283. package/src/rbac/hooks/useRoleManagement.ts +292 -12
  284. package/src/rbac/index.ts +30 -0
  285. package/src/services/OrganisationService.ts +4 -0
  286. package/src/styles/core.css +5 -5
  287. package/src/types/database.generated.ts +63 -9
  288. package/src/types/file-reference.ts +9 -0
  289. package/src/utils/__tests__/timezone.test.ts +345 -0
  290. package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
  291. package/src/utils/file-reference/index.ts +13 -2
  292. package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
  293. package/src/utils/formatting/formatting.ts +179 -0
  294. package/src/utils/index.ts +27 -1
  295. package/src/utils/location/index.ts +16 -0
  296. package/src/utils/location/location.test.ts +286 -0
  297. package/src/utils/location/location.ts +175 -0
  298. package/src/utils/security/secureDataAccess.ts +1 -1
  299. package/src/utils/storage/helpers.ts +68 -0
  300. package/src/utils/timezone/index.ts +17 -0
  301. package/src/utils/timezone/timezone.test.ts +349 -0
  302. package/src/utils/timezone/timezone.ts +281 -0
  303. package/dist/chunk-CSOFYHAG.js.map +0 -1
  304. package/dist/chunk-FUEYYMX5.js.map +0 -1
  305. package/dist/chunk-HKIT6O7W.js +0 -198
  306. package/dist/chunk-HKIT6O7W.js.map +0 -1
  307. package/dist/chunk-KUEN3HFB.js +0 -94
  308. package/dist/chunk-KUEN3HFB.js.map +0 -1
  309. package/dist/chunk-M7W4CP3M.js.map +0 -1
  310. package/dist/chunk-MI7HBHN3.js.map +0 -1
  311. package/dist/chunk-NQPMQGS2.js.map +0 -1
  312. package/dist/chunk-PWAHJW4G.js.map +0 -1
  313. package/dist/chunk-UHNYIBXL.js.map +0 -1
  314. package/dist/chunk-W22JP75J.js.map +0 -1
  315. package/dist/formatting-5wETwiGF.d.ts +0 -162
  316. /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
  317. /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
  318. /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
  319. /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
@@ -128,6 +128,8 @@ export interface PaceAppLayoutProps {
128
128
  navItems?: NavigationItem[];
129
129
  /** Show/hide event selector in the header */
130
130
  showEventSelector?: boolean;
131
+ /** Show/hide organisation selector in the header */
132
+ showOrgSelector?: boolean;
131
133
  /** Custom actions to display in the header (between event selector and user menu) */
132
134
  headerActions?: React.ReactNode;
133
135
  /** Custom logo component (overrides default logo) */
@@ -344,6 +346,7 @@ export function PaceAppLayout({
344
346
  appName,
345
347
  navItems,
346
348
  showEventSelector,
349
+ showOrgSelector,
347
350
  headerActions,
348
351
  customLogo,
349
352
  logoHref = '/dashboard',
@@ -573,16 +576,25 @@ export function PaceAppLayout({
573
576
  const hasOrganisationContext = currentScope.organisationId;
574
577
  const hasUser = !!user?.id;
575
578
 
576
- // If we have user + organisation, show items immediately (optimistic rendering)
577
- // Then filter them when permissions are fully loaded
578
- if (hasUser && hasOrganisationContext) {
579
- // We have minimum required context - proceed with filtering
580
- // Don't wait for appId or eventId - we can filter with organisationId alone
581
- // Permission checks will work with resource permissions even without appId
582
- } else if (!hasUser || !hasOrganisationContext) {
583
- // Missing required context - wait for it
579
+ // BUG FIX: When showOrgSelector is enabled, show navigation optimistically while waiting
580
+ // for organisation selection. Only hide navigation if user is not loaded.
581
+ // This prevents navigation from disappearing after initial render while waiting for org selection.
582
+ if (!hasUser) {
583
+ // User not loaded yet - show all items until user is ready
584
+ if (isMounted) {
585
+ setFilteredMenuItems(baseMenuItems);
586
+ }
587
+ return;
588
+ }
589
+
590
+ // If no organisation context yet, show items optimistically if org selector is enabled
591
+ // This allows users to see navigation while they select an organisation
592
+ // Only proceed with permission filtering once organisation is selected
593
+ if (!hasOrganisationContext) {
584
594
  if (isMounted) {
585
- setFilteredMenuItems([]);
595
+ // Show items optimistically when org selector is enabled, otherwise show all items
596
+ // This prevents navigation from disappearing while waiting for organisation selection
597
+ setFilteredMenuItems(baseMenuItems);
586
598
  }
587
599
  return;
588
600
  }
@@ -612,15 +624,6 @@ export function PaceAppLayout({
612
624
  }
613
625
  }
614
626
 
615
- // If no organisation context yet, show all items until context is ready
616
- // This prevents navigation from being empty while context loads
617
- if (!currentScope.organisationId) {
618
- if (isMounted) {
619
- setFilteredMenuItems(baseMenuItems);
620
- }
621
- return;
622
- }
623
-
624
627
  // Organisation context is ready - now filter items based on permissions
625
628
  // OPTIMIZATION: Use batch permission map instead of individual checks to avoid rate limits
626
629
  // This makes 1 call instead of N calls (where N = number of navigation items)
@@ -645,7 +648,9 @@ export function PaceAppLayout({
645
648
  const filtered = baseMenuItems.map((item) => {
646
649
  if (!item.href) return { item, hasAccess: true };
647
650
 
648
- const pageId = pageIdMapping[item.href] || item.href.slice(1) || 'home';
651
+ // Extract page ID from href: remove leading slash, fallback to 'dashboard' for root
652
+ // This matches database page names in rbac_app_pages
653
+ const pageId = pageIdMapping[item.href] || (item.href === '/' ? 'dashboard' : item.href.slice(1)) || 'dashboard';
649
654
  const permission = routePermissions[item.href] || defaultPermission;
650
655
  const fullPermission: Permission = permission.includes(':')
651
656
  ? (permission as Permission)
@@ -663,6 +668,9 @@ export function PaceAppLayout({
663
668
  .filter(({ hasAccess }) => hasAccess)
664
669
  .map(({ item }) => item);
665
670
 
671
+ // SECURITY: Never show all items if permission check fails - this would be a security risk
672
+ // If all items are filtered out, it means the user doesn't have permission to see any navigation
673
+ // This is the correct behavior - better to show nothing than show unauthorized items
666
674
  setFilteredMenuItems(accessibleItems);
667
675
  } catch (error) {
668
676
  // On error, fall back to showing all items (graceful degradation)
@@ -679,7 +687,7 @@ export function PaceAppLayout({
679
687
  return () => {
680
688
  isMounted = false;
681
689
  };
682
- }, [baseMenuItems, pageIdMapping, routePermissions, defaultPermission, can, user?.id, scope, scopeLoading, contextAppId, resolvedScope?.appId]);
690
+ }, [baseMenuItems, pageIdMapping, routePermissions, defaultPermission, can, user?.id, scope, scopeLoading, contextAppId, resolvedScope?.appId, selectedOrganisation?.id]);
683
691
 
684
692
  // NEW: Phase 2 - Enhanced Routing Features
685
693
  // Check route access for role-based routing
@@ -883,6 +891,7 @@ export function PaceAppLayout({
883
891
  }
884
892
  }}
885
893
  showEventSelector={showEventSelector}
894
+ showOrgSelector={showOrgSelector}
886
895
  showUserMenu={showUserMenu}
887
896
  className={headerClassName || "sticky top-0 z-[40] w-full"}
888
897
  />
@@ -17,6 +17,7 @@ A comprehensive application layout component that provides a consistent structur
17
17
  - Flexible content area
18
18
  - Branding support
19
19
  - **Event selector control** - can be hidden for non-event applications
20
+ - **Organisation selector control** - can be shown for multi-organisation applications
20
21
  - **Header customization** - custom logo, actions, user menu, and styling
21
22
  - **Clickable logo** - logo automatically routes to dashboard page (configurable)
22
23
 
@@ -27,6 +28,7 @@ A comprehensive application layout component that provides a consistent structur
27
28
  | `appName` | `string` | required | The name of the application to be displayed in the header |
28
29
  | `navItems` | `NavigationItem[]` | optional | Navigation items for the header menu. If not provided, uses default navigation |
29
30
  | `showEventSelector` | `boolean` | `true` | Show/hide event selector in the header |
31
+ | `showOrgSelector` | `boolean` | `false` | Show/hide organisation selector in the header |
30
32
  | `headerActions` | `React.ReactNode` | optional | Custom actions to display in the header (between event selector and user menu) |
31
33
  | `customLogo` | `React.ReactNode` | optional | Custom logo component (overrides default logo) |
32
34
  | `logoHref` | `string` | `'/dashboard'` | URL to navigate to when logo is clicked (e.g., '/dashboard', '/home') |
@@ -243,6 +245,13 @@ function App() {
243
245
  - Single-tenant applications
244
246
  - Administrative dashboards that don't depend on events
245
247
 
248
+ ### Show Organisation Selector (`showOrgSelector={true}`)
249
+ - Multi-organisation applications
250
+ - Apps where users need to switch between organisations
251
+ - Organisation management tools
252
+ - Multi-tenant applications with organisation context
253
+ - Apps requiring organisation-scoped data access
254
+
246
255
  ### Custom Header Components
247
256
  - Applications requiring custom branding
248
257
  - Apps with specific header actions (search, notifications, etc.)
@@ -83,6 +83,7 @@ describe('ProtectedRoute Component', () => {
83
83
  const defaultAuthState = {
84
84
  isAuthenticated: true,
85
85
  authLoading: false,
86
+ isLoading: false, // Combined loading state used by component
86
87
  };
87
88
 
88
89
  const defaultSessionState = {
@@ -142,7 +143,7 @@ describe('ProtectedRoute Component', () => {
142
143
  expect(screen.queryByTestId('navigate')).not.toBeInTheDocument();
143
144
  });
144
145
 
145
- it('renders outlet when events exist but none selected (allows event selector visibility)', () => {
146
+ it('renders outlet when events exist but none selected (allows event selector visibility)', async () => {
146
147
  const consoleDebugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
147
148
 
148
149
  mockUseEvents.mockReturnValue({
@@ -151,12 +152,25 @@ describe('ProtectedRoute Component', () => {
151
152
  isLoading: false,
152
153
  });
153
154
 
154
- renderWithProviders(<ProtectedRoute />);
155
+ // Wait for logger to be configured
156
+ await import('../../utils/core/logger').then(({ Logger, LogLevel }) => {
157
+ Logger.configure({
158
+ level: LogLevel.DEBUG,
159
+ includeTimestamp: false,
160
+ includeComponent: true,
161
+ });
162
+ });
163
+
164
+ renderWithProviders(<ProtectedRoute requireEvent={true} />);
155
165
 
156
166
  expect(screen.getByTestId('outlet')).toBeInTheDocument();
157
- expect(consoleDebugSpy).toHaveBeenCalledWith(
158
- expect.stringContaining('[DEBUG] [ProtectedRoute] Events available but none selected - allowing render so selector is visible')
159
- );
167
+
168
+ // Wait a bit for the logger to be called (it's called during render)
169
+ await waitFor(() => {
170
+ expect(consoleDebugSpy).toHaveBeenCalledWith(
171
+ expect.stringContaining('[DEBUG] [ProtectedRoute] Events available but none selected - allowing render so selector is visible')
172
+ );
173
+ }, { timeout: 1000 });
160
174
  });
161
175
 
162
176
  it('renders session restoration loader when session is restoring', () => {
@@ -178,6 +192,7 @@ describe('ProtectedRoute Component', () => {
178
192
  mockUseUnifiedAuth.mockReturnValue({
179
193
  isAuthenticated: false,
180
194
  authLoading: true,
195
+ isLoading: true, // Component uses isLoading, not authLoading
181
196
  });
182
197
 
183
198
  renderWithProviders(<ProtectedRoute />);
@@ -193,6 +208,7 @@ describe('ProtectedRoute Component', () => {
193
208
  mockUseUnifiedAuth.mockReturnValue({
194
209
  isAuthenticated: false,
195
210
  authLoading: true,
211
+ isLoading: true, // Component uses isLoading, not authLoading
196
212
  });
197
213
 
198
214
  renderWithProviders(<ProtectedRoute loadingFallback={customLoader} />);
@@ -207,6 +223,7 @@ describe('ProtectedRoute Component', () => {
207
223
  mockUseUnifiedAuth.mockReturnValue({
208
224
  isAuthenticated: false,
209
225
  authLoading: false,
226
+ isLoading: false,
210
227
  });
211
228
 
212
229
  renderWithProviders(<ProtectedRoute />);
@@ -222,6 +239,7 @@ describe('ProtectedRoute Component', () => {
222
239
  mockUseUnifiedAuth.mockReturnValue({
223
240
  isAuthenticated: false,
224
241
  authLoading: false,
242
+ isLoading: false,
225
243
  });
226
244
 
227
245
  renderWithProviders(<ProtectedRoute loginPath="/custom-login" />);
@@ -234,6 +252,7 @@ describe('ProtectedRoute Component', () => {
234
252
  mockUseUnifiedAuth.mockReturnValue({
235
253
  isAuthenticated: false,
236
254
  authLoading: false,
255
+ isLoading: false,
237
256
  });
238
257
 
239
258
  renderWithProviders(<ProtectedRoute />);
@@ -248,6 +267,7 @@ describe('ProtectedRoute Component', () => {
248
267
  mockUseUnifiedAuth.mockReturnValue({
249
268
  isAuthenticated: false,
250
269
  authLoading: false,
270
+ isLoading: false,
251
271
  });
252
272
 
253
273
  mockUseSessionRestoration.mockReturnValue({
@@ -273,6 +293,7 @@ describe('ProtectedRoute Component', () => {
273
293
  mockUseUnifiedAuth.mockReturnValue({
274
294
  isAuthenticated: false,
275
295
  authLoading: false,
296
+ isLoading: false,
276
297
  });
277
298
 
278
299
  mockUseSessionRestoration.mockReturnValue({
@@ -328,6 +349,7 @@ describe('ProtectedRoute Component', () => {
328
349
  mockUseUnifiedAuth.mockReturnValue({
329
350
  isAuthenticated: false,
330
351
  authLoading: true,
352
+ isLoading: true,
331
353
  });
332
354
 
333
355
  mockUseSessionRestoration.mockReturnValue({
@@ -347,6 +369,7 @@ describe('ProtectedRoute Component', () => {
347
369
  mockUseUnifiedAuth.mockReturnValue({
348
370
  isAuthenticated: false,
349
371
  authLoading: true,
372
+ isLoading: true,
350
373
  });
351
374
 
352
375
  mockUseSessionRestoration.mockReturnValue({
@@ -508,6 +531,7 @@ describe('ProtectedRoute Component', () => {
508
531
  mockUseUnifiedAuth.mockReturnValue({
509
532
  isAuthenticated: true,
510
533
  authLoading: false,
534
+ isLoading: false,
511
535
  });
512
536
 
513
537
  mockUseSessionRestoration.mockReturnValue({
@@ -527,6 +551,7 @@ describe('ProtectedRoute Component', () => {
527
551
  mockUseUnifiedAuth.mockReturnValue({
528
552
  isAuthenticated: true,
529
553
  authLoading: false,
554
+ isLoading: false,
530
555
  });
531
556
 
532
557
  mockUseSessionRestoration.mockReturnValue({
@@ -546,6 +571,7 @@ describe('ProtectedRoute Component', () => {
546
571
  mockUseUnifiedAuth.mockReturnValue({
547
572
  isAuthenticated: true,
548
573
  authLoading: true,
574
+ isLoading: true,
549
575
  });
550
576
 
551
577
  mockUseEvents.mockReturnValue({
@@ -576,7 +602,7 @@ describe('ProtectedRoute Component', () => {
576
602
  });
577
603
 
578
604
  describe('Props Configuration', () => {
579
- it('defaults requireEvent to true when not provided', () => {
605
+ it('defaults requireEvent to false when not provided', () => {
580
606
  mockUseEvents.mockReturnValue({
581
607
  selectedEvent: null,
582
608
  events: [],
@@ -585,8 +611,8 @@ describe('ProtectedRoute Component', () => {
585
611
 
586
612
  renderWithProviders(<ProtectedRoute />);
587
613
 
588
- // Should show no events error since requireEvent defaults to true
589
- expect(screen.getByTestId('alert')).toBeInTheDocument();
614
+ // Should render outlet since requireEvent defaults to false
615
+ expect(screen.getByTestId('outlet')).toBeInTheDocument();
590
616
  });
591
617
 
592
618
  it('respects requireEvent prop when set to false', () => {
@@ -607,6 +633,7 @@ describe('ProtectedRoute Component', () => {
607
633
  mockUseUnifiedAuth.mockReturnValue({
608
634
  isAuthenticated: false,
609
635
  authLoading: false,
636
+ isLoading: false,
610
637
  });
611
638
 
612
639
  renderWithProviders(<ProtectedRoute />);
@@ -619,6 +646,7 @@ describe('ProtectedRoute Component', () => {
619
646
  mockUseUnifiedAuth.mockReturnValue({
620
647
  isAuthenticated: false,
621
648
  authLoading: false,
649
+ isLoading: false,
622
650
  });
623
651
 
624
652
  renderWithProviders(<ProtectedRoute loginPath="/auth/login" />);
@@ -633,6 +661,7 @@ describe('ProtectedRoute Component', () => {
633
661
  mockUseUnifiedAuth.mockReturnValue({
634
662
  isAuthenticated: false,
635
663
  authLoading: true,
664
+ isLoading: true,
636
665
  });
637
666
 
638
667
  renderWithProviders(<ProtectedRoute />);
@@ -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
  /**
@@ -131,16 +132,113 @@ export interface ProtectedRouteProps {
131
132
  * @returns React element with route protection logic
132
133
  */
133
134
  export function ProtectedRoute({
134
- requireEvent = true,
135
+ requireEvent = false,
135
136
  allowSuperAdminBypass = false,
136
137
  noEventsFallback,
137
138
  loadingFallback,
138
139
  loginPath = '/login'
139
140
  }: ProtectedRouteProps) {
140
- const { isAuthenticated, authLoading } = useUnifiedAuth();
141
- const { selectedEvent, events, isLoading: eventLoading } = useEvents();
141
+ const { isAuthenticated, isLoading } = useUnifiedAuth();
142
+
143
+ // Always call useEvents() - UnifiedAuthProvider always includes EventServiceProvider
144
+ // Only use the values when requireEvent is true
145
+ const eventsContext = useEvents();
146
+ const selectedEvent = requireEvent ? eventsContext.selectedEvent : null;
147
+ const events = requireEvent ? (eventsContext.events || []) : [];
148
+ const eventLoading = requireEvent ? (eventsContext.isLoading || false) : false;
149
+
142
150
  const sessionRestoration = useSessionRestoration();
143
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
+
144
242
  const isRestoringSession = useMemo(() => {
145
243
  return sessionRestoration.isRestoring &&
146
244
  !sessionRestoration.restorationComplete &&
@@ -165,7 +263,8 @@ export function ProtectedRoute({
165
263
  }
166
264
 
167
265
  // Show loading state while auth is being determined (but not organisation/event loading)
168
- if (authLoading && !sessionRestoration.hasTimedOut) {
266
+ // Use isLoading (combined loading state) for consistency with simpler implementations
267
+ if (isLoading && !sessionRestoration.hasTimedOut) {
169
268
  return loadingFallback || (
170
269
  <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
171
270
  <LoadingSpinner />
@@ -174,13 +273,55 @@ export function ProtectedRoute({
174
273
  }
175
274
 
176
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)
177
283
  if (!isAuthenticated) {
284
+ // Session restoration timeout/error always redirects immediately
178
285
  if (sessionRestoration.hasTimedOut || sessionRestoration.restorationError) {
179
286
  logger.warn('ProtectedRoute', 'Session restoration failed, redirecting to login', {
180
287
  timedOut: sessionRestoration.hasTimedOut,
181
288
  error: sessionRestoration.restorationError?.message
182
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
+ );
183
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
184
325
  return <Navigate to={loginPath} replace />;
185
326
  }
186
327
 
@@ -126,6 +126,14 @@ export type { TabsProps, TabsListProps, TabsTriggerProps, TabsContentProps } fro
126
126
  export { Calendar } from './Calendar';
127
127
  export type { CalendarProps } from './Calendar';
128
128
 
129
+ // DateTimeField exports
130
+ export { DateTimeField } from './DateTimeField';
131
+ export type { DateTimeFieldProps } from './DateTimeField';
132
+
133
+ // DatePickerWithTimezone exports
134
+ export { DatePickerWithTimezone } from './DatePickerWithTimezone';
135
+ export type { DatePickerWithTimezoneProps } from './DatePickerWithTimezone';
136
+
129
137
  // Toast exports
130
138
  export {
131
139
  Toast,