@jmruthers/pace-core 0.5.188 → 0.5.190
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/core-usage-manifest.json +0 -4
- package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
- package/dist/{DataTable-GUFUNZ3N.js → DataTable-ON3IXISJ.js} +8 -8
- package/dist/{PublicPageProvider-DrLDztHt.d.ts → PublicPageProvider-C4uxosp6.d.ts} +129 -40
- package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
- package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-X5NXANVI.js} +4 -2
- package/dist/{api-YP7XD5L6.js → api-I6UCQ5S6.js} +4 -2
- package/dist/{chunk-DDM4CCYT.js → chunk-4QYC5L4K.js} +60 -35
- package/dist/chunk-4QYC5L4K.js.map +1 -0
- package/dist/{chunk-IM4QE42D.js → chunk-73HSNNOQ.js} +141 -326
- package/dist/chunk-73HSNNOQ.js.map +1 -0
- package/dist/{chunk-YHCN776L.js → chunk-DZWK57KZ.js} +2 -75
- package/dist/chunk-DZWK57KZ.js.map +1 -0
- package/dist/{chunk-3GOZZZYH.js → chunk-HQVPB5MZ.js} +238 -301
- package/dist/chunk-HQVPB5MZ.js.map +1 -0
- package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
- package/dist/chunk-HW3OVDUF.js.map +1 -0
- package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
- package/dist/chunk-I7PSE6JW.js.map +1 -0
- package/dist/{chunk-VGZZXKBR.js → chunk-J2XXC7R5.js} +280 -52
- package/dist/chunk-J2XXC7R5.js.map +1 -0
- package/dist/{chunk-UNOTYLQF.js → chunk-NIU6J6OX.js} +772 -725
- package/dist/chunk-NIU6J6OX.js.map +1 -0
- package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
- package/dist/{chunk-HEHYGYOX.js → chunk-RUYZKXOD.js} +401 -46
- package/dist/chunk-RUYZKXOD.js.map +1 -0
- package/dist/{chunk-2UUZZJFT.js → chunk-SDMHPX3X.js} +176 -160
- package/dist/{chunk-2UUZZJFT.js.map → chunk-SDMHPX3X.js.map} +1 -1
- package/dist/{chunk-IPCH26AG.js → chunk-STYK4OH2.js} +11 -11
- package/dist/chunk-STYK4OH2.js.map +1 -0
- package/dist/{chunk-EFCLXK7F.js → chunk-VVBAW5A5.js} +4201 -3809
- package/dist/chunk-VVBAW5A5.js.map +1 -0
- package/dist/chunk-Y4BUBBHD.js +614 -0
- package/dist/chunk-Y4BUBBHD.js.map +1 -0
- package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
- package/dist/chunk-ZSAAAMVR.js.map +1 -0
- package/dist/components.d.ts +3 -5
- package/dist/components.js +19 -23
- package/dist/components.js.map +1 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
- package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
- package/dist/hooks.d.ts +10 -5
- package/dist/hooks.js +14 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +13 -12
- package/dist/index.js +79 -73
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +3 -1
- package/dist/rbac/index.d.ts +76 -12
- package/dist/rbac/index.js +12 -9
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-DxIDS4bC.d.ts} +16 -9
- package/dist/utils.js +16 -16
- package/docs/README.md +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +2 -2
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +2 -2
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +4 -4
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +2 -2
- package/docs/api/classes/SecureSupabaseClient.md +21 -16
- package/docs/api/classes/StorageUtils.md +7 -4
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +1 -1
- package/docs/api/interfaces/AddressFieldRef.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +128 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +20 -6
- 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 +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +9 -9
- 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 +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +62 -16
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +2 -2
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +26 -12
- package/docs/api/interfaces/FileUploadProps.md +30 -19
- 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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
- package/docs/api/interfaces/NavigationContextType.md +9 -9
- 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 +7 -7
- 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 +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +8 -8
- package/docs/api/interfaces/PagePermissionContextType.md +8 -8
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +3 -11
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- 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 +1 -1
- 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 +1 -1
- 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/RBACPerformanceMetrics.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 +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +10 -10
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +9 -9
- package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +4 -4
- package/docs/api/interfaces/StorageFileInfo.md +7 -7
- package/docs/api/interfaces/StorageFileMetadata.md +25 -14
- package/docs/api/interfaces/StorageListOptions.md +22 -9
- package/docs/api/interfaces/StorageListResult.md +4 -4
- package/docs/api/interfaces/StorageUploadOptions.md +21 -8
- package/docs/api/interfaces/StorageUploadResult.md +6 -6
- package/docs/api/interfaces/StorageUrlOptions.md +19 -6
- 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 +53 -53
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +4 -4
- package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +155 -135
- package/docs/api-reference/components.md +72 -29
- package/docs/api-reference/providers.md +2 -2
- package/docs/api-reference/rpc-functions.md +1 -0
- package/docs/best-practices/README.md +1 -1
- package/docs/best-practices/deployment.md +8 -8
- package/docs/getting-started/examples/README.md +2 -2
- package/docs/getting-started/installation-guide.md +4 -4
- package/docs/getting-started/quick-start.md +3 -3
- package/docs/migration/MIGRATION_GUIDE.md +3 -3
- package/docs/rbac/compliance/compliance-guide.md +2 -2
- package/docs/rbac/event-based-apps.md +2 -2
- package/docs/rbac/getting-started.md +2 -2
- package/docs/rbac/quick-start.md +2 -2
- package/docs/security/README.md +4 -4
- package/docs/standards/07-rbac-and-rls-standard.md +430 -7
- package/docs/troubleshooting/README.md +2 -2
- package/docs/troubleshooting/migration.md +3 -3
- package/package.json +1 -4
- package/scripts/check-pace-core-compliance.cjs +1 -1
- package/scripts/check-pace-core-compliance.js +1 -1
- package/src/__tests__/fixtures/supabase.ts +301 -0
- package/src/__tests__/public-recipe-view.test.ts +9 -9
- package/src/__tests__/rls-policies.test.ts +197 -61
- package/src/components/AddressField/AddressField.test.tsx +42 -0
- package/src/components/AddressField/AddressField.tsx +71 -60
- package/src/components/AddressField/README.md +1 -0
- package/src/components/Alert/Alert.test.tsx +50 -10
- package/src/components/Alert/Alert.tsx +5 -3
- package/src/components/Avatar/Avatar.test.tsx +252 -226
- package/src/components/Avatar/Avatar.tsx +179 -53
- package/src/components/Avatar/index.ts +1 -1
- package/src/components/Button/Button.test.tsx +2 -1
- package/src/components/Button/Button.tsx +3 -3
- package/src/components/Calendar/Calendar.test.tsx +53 -37
- package/src/components/Calendar/Calendar.tsx +409 -82
- package/src/components/Card/Card.test.tsx +7 -4
- package/src/components/Card/Card.tsx +3 -6
- package/src/components/Checkbox/Checkbox.tsx +2 -2
- package/src/components/DataTable/components/ActionButtons.tsx +5 -5
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
- package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
- package/src/components/DataTable/components/DataTableBody.tsx +12 -12
- package/src/components/DataTable/components/DataTableCore.tsx +3 -3
- package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
- package/src/components/DataTable/components/EditableRow.tsx +2 -2
- package/src/components/DataTable/components/EmptyState.tsx +3 -3
- package/src/components/DataTable/components/GroupHeader.tsx +2 -2
- package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
- package/src/components/DataTable/components/ImportModal.tsx +4 -4
- package/src/components/DataTable/components/LoadingState.tsx +1 -1
- package/src/components/DataTable/components/PaginationControls.tsx +11 -11
- package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
- package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
- package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
- package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
- package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
- package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
- package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
- package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
- package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
- package/src/components/Dialog/Dialog.tsx +2 -2
- package/src/components/EventSelector/EventSelector.tsx +7 -7
- package/src/components/FileDisplay/FileDisplay.tsx +291 -179
- package/src/components/FileUpload/FileUpload.tsx +7 -4
- package/src/components/Header/Header.test.tsx +28 -0
- package/src/components/Header/Header.tsx +22 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
- package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +8 -8
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
- package/src/components/PaceAppLayout/test-setup.tsx +1 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
- package/src/components/Progress/Progress.test.tsx +18 -19
- package/src/components/Progress/Progress.tsx +31 -32
- package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
- package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
- package/src/components/Select/Select.tsx +5 -5
- package/src/components/Switch/Switch.test.tsx +2 -1
- package/src/components/Switch/Switch.tsx +1 -1
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/components/Tooltip/Tooltip.test.tsx +8 -2
- package/src/components/UserMenu/UserMenu.test.tsx +7 -9
- package/src/components/UserMenu/UserMenu.tsx +10 -8
- package/src/components/index.ts +2 -1
- package/src/eslint-rules/pace-core-compliance.cjs +0 -2
- package/src/eslint-rules/pace-core-compliance.js +0 -2
- package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
- package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
- package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
- package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
- package/src/hooks/__tests__/{usePublicEvent.unit.test.ts → usePublicEvent.test.ts} +28 -1
- package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
- package/src/hooks/index.ts +1 -1
- package/src/hooks/public/usePublicEvent.ts +2 -2
- package/src/hooks/public/usePublicFileDisplay.ts +173 -87
- package/src/hooks/useAppConfig.ts +24 -5
- package/src/hooks/useFileDisplay.ts +297 -34
- package/src/hooks/useFileReference.ts +56 -11
- package/src/hooks/useFileUrl.ts +1 -1
- package/src/hooks/useInactivityTracker.ts +16 -7
- package/src/hooks/usePermissionCache.test.ts +85 -8
- package/src/hooks/useQueryCache.ts +21 -0
- package/src/hooks/useSecureDataAccess.test.ts +80 -35
- package/src/hooks/useSecureDataAccess.ts +80 -37
- package/src/index.ts +2 -1
- package/src/providers/services/EventServiceProvider.tsx +37 -17
- package/src/providers/services/InactivityServiceProvider.tsx +4 -4
- package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
- package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
- package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
- package/src/rbac/api.ts +240 -36
- package/src/rbac/cache-invalidation.ts +21 -7
- package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
- package/src/rbac/components/NavigationGuard.tsx +23 -63
- package/src/rbac/components/NavigationProvider.test.tsx +52 -23
- package/src/rbac/components/NavigationProvider.tsx +13 -11
- package/src/rbac/components/PagePermissionGuard.tsx +77 -203
- package/src/rbac/components/PagePermissionProvider.tsx +13 -11
- package/src/rbac/components/PermissionEnforcer.tsx +24 -62
- package/src/rbac/components/RoleBasedRouter.tsx +14 -12
- package/src/rbac/components/SecureDataProvider.tsx +13 -11
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
- package/src/rbac/engine.ts +4 -2
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
- package/src/rbac/hooks/index.ts +3 -0
- package/src/rbac/hooks/useCan.test.ts +101 -53
- package/src/rbac/hooks/usePermissions.ts +108 -41
- package/src/rbac/hooks/useRBAC.test.ts +11 -3
- package/src/rbac/hooks/useRBAC.ts +83 -40
- package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
- package/src/rbac/hooks/useResolvedScope.ts +128 -70
- package/src/rbac/hooks/useSecureSupabase.ts +36 -19
- package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
- package/src/rbac/request-deduplication.ts +1 -1
- package/src/rbac/secureClient.ts +72 -12
- package/src/rbac/security.ts +29 -23
- package/src/rbac/types.ts +10 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
- package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +6 -1
- package/src/rbac/utils/contextValidator.ts +288 -0
- package/src/rbac/utils/eventContext.ts +48 -2
- package/src/services/EventService.ts +165 -21
- package/src/services/OrganisationService.ts +37 -2
- package/src/services/__tests__/EventService.test.ts +26 -21
- package/src/types/file-reference.ts +13 -10
- package/src/utils/app/appNameResolver.test.ts +346 -73
- package/src/utils/context/superAdminOverride.ts +58 -0
- package/src/utils/file-reference/index.ts +61 -33
- package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
- package/src/utils/storage/helpers.test.ts +1 -1
- package/src/utils/storage/helpers.ts +38 -19
- package/src/utils/storage/types.ts +15 -8
- package/src/utils/validation/__tests__/csrf.test.ts +105 -0
- package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
- package/src/vite-env.d.ts +2 -2
- package/dist/chunk-3GOZZZYH.js.map +0 -1
- package/dist/chunk-DDM4CCYT.js.map +0 -1
- package/dist/chunk-E7UAOUMY.js +0 -75
- package/dist/chunk-E7UAOUMY.js.map +0 -1
- package/dist/chunk-EFCLXK7F.js.map +0 -1
- package/dist/chunk-F2IMUDXZ.js.map +0 -1
- package/dist/chunk-HEHYGYOX.js.map +0 -1
- package/dist/chunk-IM4QE42D.js.map +0 -1
- package/dist/chunk-IPCH26AG.js.map +0 -1
- package/dist/chunk-SAUPYVLF.js.map +0 -1
- package/dist/chunk-THRPYOFK.js.map +0 -1
- package/dist/chunk-UNOTYLQF.js.map +0 -1
- package/dist/chunk-VGZZXKBR.js.map +0 -1
- package/dist/chunk-YHCN776L.js.map +0 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
- package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
- /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
- /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.js.map} +0 -0
- /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
|
@@ -19,7 +19,8 @@ export interface FileUploadProps {
|
|
|
19
19
|
supabase: SupabaseClient;
|
|
20
20
|
table_name: string;
|
|
21
21
|
record_id: string;
|
|
22
|
-
organisation_id
|
|
22
|
+
organisation_id?: string | null; // Optional for user-scoped files (e.g., profile photos)
|
|
23
|
+
userId?: string; // Optional userId for user-scoped files (required if organisation_id is not provided)
|
|
23
24
|
app_id?: string; // Optional - will be resolved from app name if not provided
|
|
24
25
|
category: FileCategory;
|
|
25
26
|
folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
|
|
@@ -51,6 +52,7 @@ export function FileUpload({
|
|
|
51
52
|
table_name,
|
|
52
53
|
record_id,
|
|
53
54
|
organisation_id,
|
|
55
|
+
userId,
|
|
54
56
|
app_id,
|
|
55
57
|
category,
|
|
56
58
|
folder,
|
|
@@ -287,7 +289,8 @@ export function FileUpload({
|
|
|
287
289
|
const result = await uploadFile({
|
|
288
290
|
table_name,
|
|
289
291
|
record_id,
|
|
290
|
-
organisation_id,
|
|
292
|
+
organisation_id: organisation_id || null,
|
|
293
|
+
userId: userId, // Pass userId prop directly - it's required for user-scoped files when organisation_id is null
|
|
291
294
|
app_id: resolvedAppId ? assertAppId(resolvedAppId) : assertAppId(''),
|
|
292
295
|
category,
|
|
293
296
|
folder,
|
|
@@ -500,7 +503,7 @@ export function FileUpload({
|
|
|
500
503
|
aria-live="polite"
|
|
501
504
|
aria-label="Uploading file"
|
|
502
505
|
>
|
|
503
|
-
<div className="animate-spin rounded-full
|
|
506
|
+
<div className="animate-spin rounded-full size-8 border-b-2 border-main-500" aria-hidden="true"></div>
|
|
504
507
|
</div>
|
|
505
508
|
)}
|
|
506
509
|
</div>
|
|
@@ -581,7 +584,7 @@ export function FileUpload({
|
|
|
581
584
|
)}
|
|
582
585
|
{isUploading && (
|
|
583
586
|
<div
|
|
584
|
-
className="animate-spin rounded-full
|
|
587
|
+
className="animate-spin rounded-full size-5 border-b-2 border-main-500"
|
|
585
588
|
role="status"
|
|
586
589
|
aria-label="Uploading"
|
|
587
590
|
aria-hidden="true"
|
|
@@ -68,6 +68,34 @@ vi.mock('../OrganisationSelector', () => ({
|
|
|
68
68
|
),
|
|
69
69
|
}));
|
|
70
70
|
|
|
71
|
+
// Mock useOrganisations hook
|
|
72
|
+
vi.mock('../../hooks/useOrganisations', () => ({
|
|
73
|
+
useOrganisations: vi.fn(() => ({
|
|
74
|
+
organisations: [
|
|
75
|
+
{
|
|
76
|
+
id: 'test-org-id',
|
|
77
|
+
name: 'Test Organisation',
|
|
78
|
+
slug: 'test-org',
|
|
79
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
80
|
+
updated_at: '2023-01-01T00:00:00Z'
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
selectedOrganisation: {
|
|
84
|
+
id: 'test-org-id',
|
|
85
|
+
name: 'Test Organisation',
|
|
86
|
+
slug: 'test-org',
|
|
87
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
88
|
+
updated_at: '2023-01-01T00:00:00Z'
|
|
89
|
+
},
|
|
90
|
+
isContextReady: true,
|
|
91
|
+
isLoading: false,
|
|
92
|
+
error: null,
|
|
93
|
+
selectOrganisation: vi.fn(),
|
|
94
|
+
refreshOrganisations: vi.fn(),
|
|
95
|
+
userMemberships: []
|
|
96
|
+
}))
|
|
97
|
+
}));
|
|
98
|
+
|
|
71
99
|
// Test data
|
|
72
100
|
const mockUser: User = {
|
|
73
101
|
id: '123',
|
|
@@ -97,6 +97,7 @@ import { UserMenu } from '../UserMenu';
|
|
|
97
97
|
import { NavigationMenu } from '../NavigationMenu';
|
|
98
98
|
import type { NavigationItem } from '../NavigationMenu';
|
|
99
99
|
import type { PasswordChangeFormError } from '../PasswordChange/PasswordChangeForm';
|
|
100
|
+
import { useOrganisations } from '../../hooks/useOrganisations';
|
|
100
101
|
|
|
101
102
|
/**
|
|
102
103
|
* Props for the Header component
|
|
@@ -255,6 +256,23 @@ export function Header({
|
|
|
255
256
|
onNavigate,
|
|
256
257
|
logoHref
|
|
257
258
|
}: HeaderProps) {
|
|
259
|
+
// Conditional wrapper for organisation selector - only show if user has organisations
|
|
260
|
+
const OrganisationSelectorConditional = () => {
|
|
261
|
+
const { organisations, isContextReady } = useOrganisations();
|
|
262
|
+
// Only show selector if user has organisations and context is ready
|
|
263
|
+
if (!isContextReady || !organisations || organisations.length === 0) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return (
|
|
267
|
+
<OrganisationSelector
|
|
268
|
+
placeholder="Select organisation"
|
|
269
|
+
className="w-64"
|
|
270
|
+
data-testid="org-selector"
|
|
271
|
+
compact={true}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
258
276
|
return (
|
|
259
277
|
<header className={cn(
|
|
260
278
|
"w-full border-b border-main-200 h-16 shadow-sm bg-main-100 ",
|
|
@@ -292,14 +310,14 @@ export function Header({
|
|
|
292
310
|
<img
|
|
293
311
|
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
|
|
294
312
|
alt={logoAlt || 'Logo'}
|
|
295
|
-
className="
|
|
313
|
+
className="size-8 shadow-md"
|
|
296
314
|
/>
|
|
297
315
|
</Link>
|
|
298
316
|
) : (
|
|
299
317
|
<img
|
|
300
318
|
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
|
|
301
319
|
alt={logoAlt || 'Logo'}
|
|
302
|
-
className="
|
|
320
|
+
className="size-8 shadow-md"
|
|
303
321
|
/>
|
|
304
322
|
)
|
|
305
323
|
)}
|
|
@@ -319,14 +337,9 @@ export function Header({
|
|
|
319
337
|
|
|
320
338
|
{/* Right side: Organisation Selector, Event Selector, Actions, and User Menu */}
|
|
321
339
|
<div className="flex items-center gap-4 ml-auto">
|
|
322
|
-
{/* Organisation Selector */}
|
|
340
|
+
{/* Organisation Selector - Only show if user has organisations */}
|
|
323
341
|
{showOrgSelector ? (
|
|
324
|
-
<
|
|
325
|
-
placeholder="Select organisation"
|
|
326
|
-
className="w-64"
|
|
327
|
-
data-testid="org-selector"
|
|
328
|
-
compact={true}
|
|
329
|
-
/>
|
|
342
|
+
<OrganisationSelectorConditional />
|
|
330
343
|
) : null}
|
|
331
344
|
|
|
332
345
|
{/* Event Selector */}
|
|
@@ -103,7 +103,7 @@ export function InactivityWarningModal({
|
|
|
103
103
|
<DialogHeader>
|
|
104
104
|
<div className="flex items-center gap-3">
|
|
105
105
|
<div className="flex-shrink-0">
|
|
106
|
-
<AlertTriangle className="
|
|
106
|
+
<AlertTriangle className="size-6 text-acc-600" />
|
|
107
107
|
</div>
|
|
108
108
|
<div>
|
|
109
109
|
<DialogTitle className="text-lg font-semibold text-main-900">
|
|
@@ -120,7 +120,7 @@ export function InactivityWarningModal({
|
|
|
120
120
|
{/* Countdown Timer */}
|
|
121
121
|
<div className="text-center">
|
|
122
122
|
<div className="inline-flex items-center gap-2 px-4 py-3 bg-acc-50 border border-acc-200 rounded-lg">
|
|
123
|
-
<Clock className="
|
|
123
|
+
<Clock className="size-5 text-acc-600" />
|
|
124
124
|
<span className="text-2xl font-mono font-bold text-acc-700">
|
|
125
125
|
{formatTime(displayTime)}
|
|
126
126
|
</span>
|
|
@@ -54,44 +54,47 @@ describe('LoadingSpinner Component', () => {
|
|
|
54
54
|
renderWithProviders(<LoadingSpinner size="sm" />);
|
|
55
55
|
|
|
56
56
|
const spinner = screen.getByRole('status');
|
|
57
|
-
|
|
57
|
+
// LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
|
|
58
|
+
expect(spinner).toHaveClass('size-4');
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
it('renders medium size spinner (default)', () => {
|
|
61
62
|
renderWithProviders(<LoadingSpinner size="md" />);
|
|
62
63
|
|
|
63
64
|
const spinner = screen.getByRole('status');
|
|
64
|
-
|
|
65
|
+
// LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
|
|
66
|
+
expect(spinner).toHaveClass('size-6');
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
it('renders large size spinner', () => {
|
|
68
70
|
renderWithProviders(<LoadingSpinner size="lg" />);
|
|
69
71
|
|
|
70
72
|
const spinner = screen.getByRole('status');
|
|
71
|
-
|
|
73
|
+
// LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
|
|
74
|
+
expect(spinner).toHaveClass('size-8');
|
|
72
75
|
});
|
|
73
76
|
|
|
74
77
|
it('uses medium size as default when no size specified', () => {
|
|
75
78
|
renderWithProviders(<LoadingSpinner />);
|
|
76
79
|
|
|
77
80
|
const spinner = screen.getByRole('status');
|
|
78
|
-
|
|
81
|
+
// LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
|
|
82
|
+
expect(spinner).toHaveClass('size-6');
|
|
79
83
|
});
|
|
80
84
|
|
|
81
85
|
it('applies correct size classes for each variant', () => {
|
|
82
86
|
const sizeTests: Array<{ size: LoadingSpinnerProps['size']; expectedClasses: string[] }> = [
|
|
83
|
-
{ size: 'sm',
|
|
84
|
-
{ size: 'md',
|
|
85
|
-
{ size: 'lg',
|
|
87
|
+
{ size: 'sm', expectedClass: 'size-4' },
|
|
88
|
+
{ size: 'md', expectedClass: 'size-6' },
|
|
89
|
+
{ size: 'lg', expectedClass: 'size-8' },
|
|
86
90
|
];
|
|
87
91
|
|
|
88
|
-
sizeTests.forEach(({ size,
|
|
92
|
+
sizeTests.forEach(({ size, expectedClass }) => {
|
|
89
93
|
const { unmount } = renderWithProviders(<LoadingSpinner size={size} />);
|
|
90
94
|
|
|
91
95
|
const spinner = screen.getByRole('status');
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
96
|
+
// LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
|
|
97
|
+
expect(spinner).toHaveClass(expectedClass);
|
|
95
98
|
|
|
96
99
|
unmount();
|
|
97
100
|
});
|
|
@@ -246,8 +249,9 @@ describe('LoadingSpinner Component', () => {
|
|
|
246
249
|
// Component defaults to 'md' size when invalid size is passed
|
|
247
250
|
expect(spinner).toHaveClass('inline-block', 'animate-spin');
|
|
248
251
|
// Should default to medium size classes when invalid size is passed
|
|
249
|
-
|
|
250
|
-
expect(spinner).
|
|
252
|
+
// LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
|
|
253
|
+
expect(spinner).toHaveClass('size-6');
|
|
254
|
+
expect(spinner).not.toHaveClass('size-4', 'size-8');
|
|
251
255
|
});
|
|
252
256
|
|
|
253
257
|
it('handles null className gracefully', () => {
|
|
@@ -313,7 +317,8 @@ describe('LoadingSpinner Component', () => {
|
|
|
313
317
|
|
|
314
318
|
expect(updatedSpinner).toBeInTheDocument();
|
|
315
319
|
expect(updatedScreenReaderText).toBeInTheDocument();
|
|
316
|
-
|
|
320
|
+
// LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
|
|
321
|
+
expect(updatedSpinner).toHaveClass('size-8', 'custom');
|
|
317
322
|
});
|
|
318
323
|
});
|
|
319
324
|
|
|
@@ -85,9 +85,9 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
|
|
85
85
|
className = ''
|
|
86
86
|
}) => {
|
|
87
87
|
const sizeClasses: Record<'sm' | 'md' | 'lg', string> = {
|
|
88
|
-
sm: '
|
|
89
|
-
md: '
|
|
90
|
-
lg: '
|
|
88
|
+
sm: 'size-4',
|
|
89
|
+
md: 'size-6',
|
|
90
|
+
lg: 'size-8'
|
|
91
91
|
};
|
|
92
92
|
|
|
93
93
|
// Ensure we always have a valid size class, defaulting to 'md' if invalid
|
|
@@ -95,8 +95,8 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
|
|
95
95
|
const sizeClass = sizeClasses[validSize];
|
|
96
96
|
|
|
97
97
|
return (
|
|
98
|
-
<
|
|
98
|
+
<canvas className={`inline-block animate-spin rounded-full border-2 border-solid border-current border-r-transparent motion-reduce:animate-[spin_1.5s_linear_infinite] ${sizeClass} ${className}`.trim()} role="status">
|
|
99
99
|
<span className="sr-only">Loading...</span>
|
|
100
|
-
</
|
|
100
|
+
</canvas>
|
|
101
101
|
);
|
|
102
102
|
};
|
|
@@ -373,6 +373,40 @@ describe('NavigationMenu Component', () => {
|
|
|
373
373
|
expect(dashboardItem).toBeInTheDocument();
|
|
374
374
|
}, { interval: 10 });
|
|
375
375
|
});
|
|
376
|
+
|
|
377
|
+
it('trusts pre-filtered items while still hiding meta-hidden entries', async () => {
|
|
378
|
+
const user = userEvent.setup();
|
|
379
|
+
|
|
380
|
+
mockUsePermissions.mockReturnValue({
|
|
381
|
+
permissions: {},
|
|
382
|
+
isLoading: false,
|
|
383
|
+
error: null,
|
|
384
|
+
hasPermission: vi.fn(() => false),
|
|
385
|
+
hasAnyPermission: vi.fn(() => false),
|
|
386
|
+
hasAllPermissions: vi.fn(() => false),
|
|
387
|
+
refetch: vi.fn(),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
renderWithProviders(
|
|
391
|
+
<NavigationMenu
|
|
392
|
+
items={[
|
|
393
|
+
{ id: 'home', label: 'Home', href: '/' },
|
|
394
|
+
{ id: 'hidden', label: 'Hidden', href: '/hidden', meta: { hidden: true } },
|
|
395
|
+
]}
|
|
396
|
+
itemsPreFiltered
|
|
397
|
+
onNavigate={mockNavigate}
|
|
398
|
+
buttonText="Menu"
|
|
399
|
+
/>
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
await user.click(screen.getByRole('combobox'));
|
|
403
|
+
|
|
404
|
+
await waitFor(() => {
|
|
405
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
406
|
+
}, { interval: 10 });
|
|
407
|
+
|
|
408
|
+
expect(screen.queryByText('Hidden')).not.toBeInTheDocument();
|
|
409
|
+
});
|
|
376
410
|
});
|
|
377
411
|
|
|
378
412
|
// Hierarchical mode tests
|
|
@@ -931,6 +965,46 @@ describe('NavigationMenu Component', () => {
|
|
|
931
965
|
expect.stringContaining('Insufficient permissions')
|
|
932
966
|
);
|
|
933
967
|
});
|
|
968
|
+
|
|
969
|
+
it('blocks navigation and reports violations when pre-filtered items lack permission', async () => {
|
|
970
|
+
const user = userEvent.setup();
|
|
971
|
+
|
|
972
|
+
mockUsePermissions.mockReturnValue({
|
|
973
|
+
permissions: { 'read:page.restricted': true } as any,
|
|
974
|
+
isLoading: false,
|
|
975
|
+
error: null,
|
|
976
|
+
hasPermission: vi.fn(() => false),
|
|
977
|
+
hasAnyPermission: vi.fn(() => false),
|
|
978
|
+
hasAllPermissions: vi.fn(() => false),
|
|
979
|
+
refetch: vi.fn(),
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
renderWithProviders(
|
|
983
|
+
<NavigationMenu
|
|
984
|
+
items={[{ id: 'restricted', label: 'Restricted', href: '/restricted', permissions: ['restricted:read'] }]}
|
|
985
|
+
onNavigate={mockNavigate}
|
|
986
|
+
onNavigationAccessDenied={mockOnNavigationAccessDenied}
|
|
987
|
+
onStrictModeViolation={mockOnStrictModeViolation}
|
|
988
|
+
itemsPreFiltered
|
|
989
|
+
strictMode
|
|
990
|
+
buttonText="Menu"
|
|
991
|
+
/>
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
await user.click(screen.getByRole('combobox'));
|
|
995
|
+
|
|
996
|
+
const restrictedItem = await screen.findByText('Restricted');
|
|
997
|
+
await user.click(restrictedItem);
|
|
998
|
+
|
|
999
|
+
expect(mockNavigate).not.toHaveBeenCalled();
|
|
1000
|
+
expect(mockOnNavigationAccessDenied).toHaveBeenCalledWith(
|
|
1001
|
+
expect.objectContaining({ id: 'restricted' })
|
|
1002
|
+
);
|
|
1003
|
+
expect(mockOnStrictModeViolation).toHaveBeenCalledWith(
|
|
1004
|
+
expect.objectContaining({ id: 'restricted' }),
|
|
1005
|
+
'Insufficient permissions'
|
|
1006
|
+
);
|
|
1007
|
+
});
|
|
934
1008
|
});
|
|
935
1009
|
|
|
936
1010
|
// Accessibility tests
|
|
@@ -1210,6 +1284,30 @@ describe('NavigationMenu Component', () => {
|
|
|
1210
1284
|
expect(logoutItem.closest('[role="option"]')).toHaveAttribute('data-disabled', 'true');
|
|
1211
1285
|
});
|
|
1212
1286
|
|
|
1287
|
+
it('handles space key activation for hierarchical leaf items', async () => {
|
|
1288
|
+
const user = userEvent.setup();
|
|
1289
|
+
const leafOnlyItems: NavigationItem[] = [
|
|
1290
|
+
{ id: 'leaf', label: 'Leaf', href: '/leaf' },
|
|
1291
|
+
];
|
|
1292
|
+
|
|
1293
|
+
renderWithProviders(
|
|
1294
|
+
<NavigationMenu
|
|
1295
|
+
items={leafOnlyItems}
|
|
1296
|
+
mode="hierarchical"
|
|
1297
|
+
onNavigate={mockNavigate}
|
|
1298
|
+
/>
|
|
1299
|
+
);
|
|
1300
|
+
|
|
1301
|
+
const leafLink = screen.getByText('Leaf');
|
|
1302
|
+
leafLink.focus();
|
|
1303
|
+
|
|
1304
|
+
await user.keyboard(' ');
|
|
1305
|
+
|
|
1306
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
1307
|
+
expect.objectContaining({ id: 'leaf', href: '/leaf' })
|
|
1308
|
+
);
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1213
1311
|
it('forwards ref correctly', () => {
|
|
1214
1312
|
const ref = React.createRef<HTMLDivElement>();
|
|
1215
1313
|
renderWithProviders(
|
|
@@ -1305,6 +1403,35 @@ describe('NavigationMenu Component', () => {
|
|
|
1305
1403
|
const listbox = screen.getByRole('listbox');
|
|
1306
1404
|
expect(listbox.childNodes.length).toBe(0);
|
|
1307
1405
|
});
|
|
1406
|
+
|
|
1407
|
+
it('surfaces items when permission map is empty but scope is available', async () => {
|
|
1408
|
+
const user = userEvent.setup();
|
|
1409
|
+
|
|
1410
|
+
mockUsePermissions.mockReturnValue({
|
|
1411
|
+
permissions: {},
|
|
1412
|
+
isLoading: false,
|
|
1413
|
+
error: null,
|
|
1414
|
+
hasPermission: vi.fn(() => false),
|
|
1415
|
+
hasAnyPermission: vi.fn(() => false),
|
|
1416
|
+
hasAllPermissions: vi.fn(() => false),
|
|
1417
|
+
refetch: vi.fn(),
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
renderWithProviders(
|
|
1421
|
+
<NavigationMenu
|
|
1422
|
+
items={basicNavItems}
|
|
1423
|
+
onNavigate={mockNavigate}
|
|
1424
|
+
buttonText="Menu"
|
|
1425
|
+
/>
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
await user.click(screen.getByRole('combobox'));
|
|
1429
|
+
|
|
1430
|
+
await waitFor(() => {
|
|
1431
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
1432
|
+
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
|
1433
|
+
}, { interval: 10 });
|
|
1434
|
+
});
|
|
1308
1435
|
});
|
|
1309
1436
|
|
|
1310
1437
|
// Audit logging tests
|
|
@@ -1369,4 +1496,3 @@ describe('NavigationMenu Component', () => {
|
|
|
1369
1496
|
});
|
|
1370
1497
|
});
|
|
1371
1498
|
});
|
|
1372
|
-
mockUseUnifiedAuthFn.mockImplementation(() => mockAuthContext);
|
|
@@ -184,7 +184,7 @@ export function OrganisationSelector({
|
|
|
184
184
|
return (
|
|
185
185
|
<div className={`space-y-2 ${className}`}>
|
|
186
186
|
<Alert variant="destructive">
|
|
187
|
-
<AlertCircle className="
|
|
187
|
+
<AlertCircle className="size-4" />
|
|
188
188
|
<AlertDescription>
|
|
189
189
|
Failed to load organisations: {orgError.message}
|
|
190
190
|
</AlertDescription>
|
|
@@ -197,7 +197,7 @@ export function OrganisationSelector({
|
|
|
197
197
|
disabled={isLoading}
|
|
198
198
|
className="w-full"
|
|
199
199
|
>
|
|
200
|
-
<RefreshCw className={`
|
|
200
|
+
<RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
|
201
201
|
Retry
|
|
202
202
|
</Button>
|
|
203
203
|
)}
|
|
@@ -211,7 +211,7 @@ export function OrganisationSelector({
|
|
|
211
211
|
return (
|
|
212
212
|
<div className={`space-y-2 ${className}`}>
|
|
213
213
|
<Alert>
|
|
214
|
-
<Building2 className="
|
|
214
|
+
<Building2 className="size-4" />
|
|
215
215
|
<AlertDescription>
|
|
216
216
|
No organisations available. Please contact your administrator to be added to an organisation.
|
|
217
217
|
</AlertDescription>
|
|
@@ -224,7 +224,7 @@ export function OrganisationSelector({
|
|
|
224
224
|
disabled={isLoading}
|
|
225
225
|
className="w-full"
|
|
226
226
|
>
|
|
227
|
-
<RefreshCw className={`
|
|
227
|
+
<RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
|
228
228
|
Check Again
|
|
229
229
|
</Button>
|
|
230
230
|
)}
|
|
@@ -237,7 +237,7 @@ export function OrganisationSelector({
|
|
|
237
237
|
// Switch error display
|
|
238
238
|
const switchErrorDisplay = switchError && (
|
|
239
239
|
<Alert variant="destructive" className="mt-2">
|
|
240
|
-
<AlertCircle className="
|
|
240
|
+
<AlertCircle className="size-4" />
|
|
241
241
|
<AlertDescription>{switchError}</AlertDescription>
|
|
242
242
|
</Alert>
|
|
243
243
|
);
|
|
@@ -255,7 +255,7 @@ export function OrganisationSelector({
|
|
|
255
255
|
{isLoading ? (
|
|
256
256
|
<LoadingSpinner size="sm" />
|
|
257
257
|
) : (
|
|
258
|
-
<Building2 className="
|
|
258
|
+
<Building2 className="size-4 text-muted-foreground" />
|
|
259
259
|
)}
|
|
260
260
|
<SelectValue placeholder={placeholder} />
|
|
261
261
|
</div>
|
|
@@ -274,7 +274,7 @@ export function OrganisationSelector({
|
|
|
274
274
|
>
|
|
275
275
|
<div className="flex items-center justify-between w-full">
|
|
276
276
|
<div className="flex items-center gap-2">
|
|
277
|
-
<Building2 className="
|
|
277
|
+
<Building2 className="size-4" />
|
|
278
278
|
<div className="flex flex-col">
|
|
279
279
|
<span className="font-medium">{org.display_name}</span>
|
|
280
280
|
{!compact && org.description && (
|
|
@@ -286,7 +286,7 @@ export function OrganisationSelector({
|
|
|
286
286
|
</div>
|
|
287
287
|
{showRole && (
|
|
288
288
|
<div className="flex items-center gap-1 ml-4">
|
|
289
|
-
<Shield className="
|
|
289
|
+
<Shield className="size-3 text-muted-foreground" />
|
|
290
290
|
<span className="text-xs text-muted-foreground capitalize">
|
|
291
291
|
{userRole?.replace('_', ' ') || 'No Role'}
|
|
292
292
|
</span>
|
|
@@ -48,8 +48,10 @@ const mockOrganisationContext = vi.hoisted(() => {
|
|
|
48
48
|
updated_at: '2023-01-01T00:00:00Z'
|
|
49
49
|
}],
|
|
50
50
|
isLoading: false,
|
|
51
|
+
isContextReady: true,
|
|
51
52
|
error: null,
|
|
52
53
|
hasValidOrganisationContext: true,
|
|
54
|
+
isContextReady: true,
|
|
53
55
|
setSelectedOrganisation: vi.fn(),
|
|
54
56
|
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
55
57
|
getUserRole: vi.fn().mockReturnValue('member'),
|
|
@@ -94,6 +96,7 @@ const mockUseUnifiedAuthFn = vi.hoisted(() => vi.fn(() => {
|
|
|
94
96
|
selectedOrganisation: mockOrganisationObj(),
|
|
95
97
|
selectedEvent: null,
|
|
96
98
|
isLoading: false,
|
|
99
|
+
isContextReady: true,
|
|
97
100
|
error: null,
|
|
98
101
|
isAuthenticated: true,
|
|
99
102
|
selectedOrganisationId: 'test-org-id',
|
|
@@ -117,6 +120,7 @@ vi.mock('../../hooks/services/useOrganisationService', () => ({
|
|
|
117
120
|
getOrganisations: () => [mockOrganisationObj()],
|
|
118
121
|
getUserMemberships: () => mockOrganisationContext().userMemberships,
|
|
119
122
|
isLoading: () => false,
|
|
123
|
+
isContextReady: () => true,
|
|
120
124
|
getError: () => null,
|
|
121
125
|
hasValidOrganisationContext: () => true,
|
|
122
126
|
setSelectedOrganisation: vi.fn(),
|
|
@@ -75,8 +75,10 @@ const mockOrganisationContext = {
|
|
|
75
75
|
updated_at: '2023-01-01T00:00:00Z'
|
|
76
76
|
}],
|
|
77
77
|
isLoading: false,
|
|
78
|
+
isContextReady: true,
|
|
78
79
|
error: null,
|
|
79
80
|
hasValidOrganisationContext: true,
|
|
81
|
+
isContextReady: true,
|
|
80
82
|
setSelectedOrganisation: vi.fn(),
|
|
81
83
|
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
82
84
|
getUserRole: vi.fn().mockReturnValue('member'),
|
|
@@ -94,6 +96,7 @@ vi.mock('../../hooks/services/useOrganisationService', () => ({
|
|
|
94
96
|
getOrganisations: () => [mockOrganisation],
|
|
95
97
|
getUserMemberships: () => mockOrganisationContext.userMemberships,
|
|
96
98
|
isLoading: () => false,
|
|
99
|
+
isContextReady: () => true,
|
|
97
100
|
getError: () => null,
|
|
98
101
|
hasValidOrganisationContext: () => true,
|
|
99
102
|
setSelectedOrganisation: vi.fn(),
|
|
@@ -112,8 +112,10 @@ const mockOrganisationContext = {
|
|
|
112
112
|
updated_at: '2023-01-01T00:00:00Z'
|
|
113
113
|
}],
|
|
114
114
|
isLoading: false,
|
|
115
|
+
isContextReady: true,
|
|
115
116
|
error: null,
|
|
116
117
|
hasValidOrganisationContext: true,
|
|
118
|
+
isContextReady: true,
|
|
117
119
|
setSelectedOrganisation: vi.fn(),
|
|
118
120
|
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
119
121
|
getUserRole: vi.fn().mockReturnValue('member'),
|
|
@@ -131,6 +133,7 @@ vi.mock('../../hooks/services/useOrganisationService', () => ({
|
|
|
131
133
|
getOrganisations: () => [mockOrganisation],
|
|
132
134
|
getUserMemberships: () => mockOrganisationContext.userMemberships,
|
|
133
135
|
isLoading: () => false,
|
|
136
|
+
isContextReady: () => true,
|
|
134
137
|
getError: () => null,
|
|
135
138
|
hasValidOrganisationContext: () => true,
|
|
136
139
|
setSelectedOrganisation: vi.fn(),
|
|
@@ -866,13 +866,18 @@ describe('PaceAppLayout Component', () => {
|
|
|
866
866
|
);
|
|
867
867
|
|
|
868
868
|
await waitFor(() => {
|
|
869
|
-
// useCan is called with userId, scope, permission, pageId, useCache
|
|
869
|
+
// useCan is called with userId, scope, permission, pageId, useCache, appName
|
|
870
870
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
871
871
|
'user-123',
|
|
872
|
-
expect.objectContaining({
|
|
872
|
+
expect.objectContaining({
|
|
873
|
+
organisationId: 'org-123',
|
|
874
|
+
eventId: 'event-123',
|
|
875
|
+
appId: 'app-123',
|
|
876
|
+
}),
|
|
873
877
|
'update:page.dashboard-page',
|
|
874
878
|
'dashboard-page',
|
|
875
|
-
true
|
|
879
|
+
true,
|
|
880
|
+
'Test App'
|
|
876
881
|
);
|
|
877
882
|
}, { timeout: 2000 });
|
|
878
883
|
}, { timeout: 3000 });
|
|
@@ -893,14 +898,19 @@ describe('PaceAppLayout Component', () => {
|
|
|
893
898
|
);
|
|
894
899
|
|
|
895
900
|
await waitFor(() => {
|
|
896
|
-
// useCan is called with userId, scope, permission, pageId, useCache
|
|
901
|
+
// useCan is called with userId, scope, permission, pageId, useCache, appName
|
|
897
902
|
// Uses defaultPermission "create" since /dashboard is not in routePermissions
|
|
898
903
|
expect(mockUseCan).toHaveBeenCalledWith(
|
|
899
904
|
'user-123',
|
|
900
|
-
expect.objectContaining({
|
|
905
|
+
expect.objectContaining({
|
|
906
|
+
organisationId: 'org-123',
|
|
907
|
+
eventId: 'event-123',
|
|
908
|
+
appId: 'app-123',
|
|
909
|
+
}),
|
|
901
910
|
'create:page.dashboard',
|
|
902
911
|
'dashboard',
|
|
903
|
-
true
|
|
912
|
+
true,
|
|
913
|
+
'Test App'
|
|
904
914
|
);
|
|
905
915
|
}, { timeout: 2000 });
|
|
906
916
|
}, { timeout: 3000 });
|