@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.
- package/CHANGELOG.md +38 -0
- package/README.md +60 -1
- package/core-usage-manifest.json +312 -0
- package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
- package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
- package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +23 -10
- package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
- package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
- package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
- package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
- package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
- package/dist/{chunk-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
- package/dist/chunk-DAGICKHT.js.map +1 -0
- package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
- package/dist/chunk-FXFJRTKI.js.map +1 -0
- package/dist/{chunk-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
- package/dist/chunk-GRIQLQ52.js.map +1 -0
- package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
- package/dist/chunk-HDCUMOOI.js.map +1 -0
- package/dist/chunk-HESYZWZW.js +388 -0
- package/dist/chunk-HESYZWZW.js.map +1 -0
- package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
- package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
- package/dist/{chunk-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
- package/dist/chunk-OALXJH4Y.js.map +1 -0
- package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
- package/dist/chunk-TC7D3CR3.js.map +1 -0
- package/dist/chunk-THRPYOFK.js +215 -0
- package/dist/chunk-THRPYOFK.js.map +1 -0
- package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
- package/dist/chunk-U6WNSFX5.js.map +1 -0
- package/dist/{chunk-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
- package/dist/chunk-UQWSHFVX.js.map +1 -0
- package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
- package/dist/components.d.ts +182 -6
- package/dist/components.js +157 -11
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
- package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
- package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
- package/dist/hooks.d.ts +52 -15
- package/dist/hooks.js +12 -22
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +82 -18
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -1
- package/dist/rbac/index.d.ts +206 -15
- package/dist/rbac/index.js +28 -6
- package/dist/timezone-_pgH8qrY.d.ts +530 -0
- package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
- package/dist/utils.d.ts +110 -152
- package/dist/utils.js +128 -138
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +60 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +178 -0
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +54 -0
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +18 -2
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +30 -0
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
- package/docs/api/interfaces/DatabaseIssue.md +41 -0
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +6 -6
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +48 -8
- package/docs/api/interfaces/FileUploadProps.md +46 -13
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +62 -0
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +52 -0
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +4 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +5 -5
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +41 -0
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
- package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +746 -50
- package/docs/api-reference/components.md +26 -12
- package/docs/api-reference/hooks.md +111 -0
- package/docs/api-reference/rpc-functions.md +1 -1
- package/docs/api-reference/utilities.md +184 -0
- package/docs/getting-started/installation-guide.md +75 -16
- package/docs/getting-started/quick-start.md +61 -11
- package/docs/implementation-guides/authentication.md +88 -12
- package/docs/implementation-guides/file-reference-system.md +26 -3
- package/docs/implementation-guides/file-upload-storage.md +30 -1
- package/docs/rbac/README.md +1 -0
- package/docs/rbac/compliance/compliance-guide.md +544 -0
- package/docs/rbac/getting-started.md +158 -33
- package/docs/standards/pace-core-compliance.md +432 -0
- package/eslint-config-pace-core.cjs +93 -0
- package/package.json +15 -3
- package/scripts/analyze-bundle.js +232 -0
- package/scripts/build-css.js +56 -0
- package/scripts/build-docs-incremental.js +1015 -0
- package/scripts/check-pace-core-compliance.cjs +2353 -0
- package/scripts/check-pace-core-compliance.js +512 -0
- package/scripts/generate-docs.js +157 -0
- package/scripts/setup-build-cache.js +73 -0
- package/scripts/utils/command-runner.js +131 -0
- package/scripts/utils/env.js +33 -0
- package/scripts/utils/index.js +10 -0
- package/scripts/utils/logger.js +88 -0
- package/scripts/utils/path-helpers.js +37 -0
- package/scripts/validate-formats.js +133 -0
- package/scripts/validate-master.js +155 -0
- package/scripts/validate-pre-publish.js +140 -0
- package/scripts/validate-theme.js +142 -0
- package/src/components/Calendar/Calendar.tsx +8 -1
- package/src/components/Card/Card.tsx +47 -8
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
- package/src/components/DatePickerWithTimezone/README.md +135 -0
- package/src/components/DatePickerWithTimezone/index.ts +10 -0
- package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
- package/src/components/DateTimeField/DateTimeField.tsx +232 -0
- package/src/components/DateTimeField/README.md +148 -0
- package/src/components/DateTimeField/index.ts +10 -0
- package/src/components/FileUpload/FileUpload.test.tsx +2 -0
- package/src/components/FileUpload/FileUpload.tsx +10 -1
- package/src/components/Header/Header.test.tsx +47 -18
- package/src/components/Header/Header.tsx +22 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
- package/src/components/PaceAppLayout/README.md +9 -0
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
- package/src/components/index.ts +8 -0
- package/src/eslint-rules/pace-core-compliance.cjs +406 -0
- package/src/eslint-rules/pace-core-compliance.js +640 -0
- package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useFileReference.test.ts +2 -0
- package/src/hooks/useFormDialog.ts +147 -0
- package/src/hooks/usePreventTabReload.ts +106 -0
- package/src/hooks/useSecureDataAccess.ts +2 -2
- package/src/index.ts +27 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
- package/src/rbac/compliance/database-validator.ts +165 -0
- package/src/rbac/compliance/index.ts +38 -0
- package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
- package/src/rbac/compliance/runtime-compliance.ts +77 -0
- package/src/rbac/compliance/setup-validator.ts +131 -0
- package/src/rbac/components/PagePermissionGuard.tsx +8 -64
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
- package/src/rbac/docs/event-based-apps.md +285 -0
- package/src/rbac/errors.ts +11 -0
- package/src/rbac/hooks/useRoleManagement.ts +292 -12
- package/src/rbac/index.ts +30 -0
- package/src/services/OrganisationService.ts +4 -0
- package/src/styles/core.css +5 -5
- package/src/types/database.generated.ts +63 -9
- package/src/types/file-reference.ts +9 -0
- package/src/utils/__tests__/timezone.test.ts +345 -0
- package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
- package/src/utils/file-reference/index.ts +13 -2
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
- package/src/utils/formatting/formatting.ts +179 -0
- package/src/utils/index.ts +27 -1
- package/src/utils/location/index.ts +16 -0
- package/src/utils/location/location.test.ts +286 -0
- package/src/utils/location/location.ts +175 -0
- package/src/utils/security/secureDataAccess.ts +1 -1
- package/src/utils/storage/helpers.ts +68 -0
- package/src/utils/timezone/index.ts +17 -0
- package/src/utils/timezone/timezone.test.ts +349 -0
- package/src/utils/timezone/timezone.ts +281 -0
- package/dist/chunk-CSOFYHAG.js.map +0 -1
- package/dist/chunk-FUEYYMX5.js.map +0 -1
- package/dist/chunk-HKIT6O7W.js +0 -198
- package/dist/chunk-HKIT6O7W.js.map +0 -1
- package/dist/chunk-KUEN3HFB.js +0 -94
- package/dist/chunk-KUEN3HFB.js.map +0 -1
- package/dist/chunk-M7W4CP3M.js.map +0 -1
- package/dist/chunk-MI7HBHN3.js.map +0 -1
- package/dist/chunk-NQPMQGS2.js.map +0 -1
- package/dist/chunk-PWAHJW4G.js.map +0 -1
- package/dist/chunk-UHNYIBXL.js.map +0 -1
- package/dist/chunk-W22JP75J.js.map +0 -1
- package/dist/formatting-5wETwiGF.d.ts +0 -162
- /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
- /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
- /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
|
-
//
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
|
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
|
|
589
|
-
expect(screen.getByTestId('
|
|
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 =
|
|
135
|
+
requireEvent = false,
|
|
135
136
|
allowSuperAdminBypass = false,
|
|
136
137
|
noEventsFallback,
|
|
137
138
|
loadingFallback,
|
|
138
139
|
loginPath = '/login'
|
|
139
140
|
}: ProtectedRouteProps) {
|
|
140
|
-
const { isAuthenticated,
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
package/src/components/index.ts
CHANGED
|
@@ -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,
|