@jmruthers/pace-core 0.5.189 → 0.5.191
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-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
- package/dist/{DataTable-GUFUNZ3N.js → DataTable-WKRZD47S.js} +8 -8
- package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +84 -25
- package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
- package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-FTSG5XH7.js} +4 -2
- package/dist/{api-YP7XD5L6.js → api-IHKALJZD.js} +4 -2
- package/dist/{chunk-VGZZXKBR.js → chunk-6LTQQAT6.js} +351 -157
- package/dist/chunk-6LTQQAT6.js.map +1 -0
- package/dist/{chunk-MX64ZF6I.js → chunk-6TQDD426.js} +15 -15
- package/dist/chunk-6TQDD426.js.map +1 -0
- package/dist/{chunk-YHCN776L.js → chunk-G37KK66H.js} +2 -75
- package/dist/chunk-G37KK66H.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-IM4QE42D.js → chunk-LOMZXPSN.js} +141 -326
- package/dist/chunk-LOMZXPSN.js.map +1 -0
- package/dist/chunk-OETXORNB.js +614 -0
- package/dist/chunk-OETXORNB.js.map +1 -0
- package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
- package/dist/{chunk-HEHYGYOX.js → chunk-ROXMHMY2.js} +403 -46
- package/dist/chunk-ROXMHMY2.js.map +1 -0
- package/dist/{chunk-2UUZZJFT.js → chunk-ULHIJK66.js} +228 -177
- package/dist/{chunk-2UUZZJFT.js.map → chunk-ULHIJK66.js.map} +1 -1
- package/dist/{chunk-YGPFYGA6.js → chunk-VKB2CO4Z.js} +838 -503
- package/dist/chunk-VKB2CO4Z.js.map +1 -0
- package/dist/{chunk-3GOZZZYH.js → chunk-VRGWKHDB.js} +238 -301
- package/dist/chunk-VRGWKHDB.js.map +1 -0
- package/dist/{chunk-UCQSRW7Z.js → chunk-XNYQOL3Z.js} +431 -384
- package/dist/chunk-XNYQOL3Z.js.map +1 -0
- package/dist/{chunk-DDM4CCYT.js → chunk-XYXSXPUK.js} +79 -59
- package/dist/chunk-XYXSXPUK.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 +5 -6
- package/dist/components.js +19 -19
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
- 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 +20 -15
- package/dist/hooks.js +14 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +17 -15
- package/dist/index.js +86 -81
- 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 +77 -13
- package/dist/rbac/index.js +12 -9
- package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +17 -10
- package/dist/utils.d.ts +8 -8
- 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 +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +5 -5
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +2 -2
- package/docs/api/classes/SecureSupabaseClient.md +25 -20
- 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 +1 -1
- 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 +2 -2
- 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 +2 -2
- 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 +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 +5 -5
- 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 +165 -106
- package/docs/api-reference/components.md +15 -7
- 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/migration/README.md +18 -0
- package/docs/migration/database-changes-december-2025.md +767 -0
- package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
- 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 -3
- 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 +19 -19
- package/src/__tests__/rls-policies.test.ts +210 -74
- package/src/components/AddressField/AddressField.test.tsx +42 -0
- package/src/components/AddressField/AddressField.tsx +71 -60
- package/src/components/AddressField/README.md +7 -6
- package/src/components/Alert/Alert.test.tsx +50 -10
- package/src/components/Alert/Alert.tsx +5 -3
- package/src/components/Avatar/Avatar.test.tsx +95 -43
- package/src/components/Avatar/Avatar.tsx +16 -16
- 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 +42 -22
- 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.test.tsx +4 -1
- package/src/components/Select/Select.tsx +65 -20
- 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.tsx +3 -3
- 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.simple.test.ts +1 -1
- package/src/hooks/__tests__/usePublicEvent.test.ts +608 -0
- package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +67 -24
- package/src/hooks/index.ts +1 -1
- package/src/hooks/public/usePublicEvent.ts +10 -10
- package/src/hooks/public/usePublicFileDisplay.ts +173 -87
- package/src/hooks/useAppConfig.ts +24 -5
- package/src/hooks/useFileDisplay.ts +298 -36
- 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 +27 -6
- package/src/hooks/useSecureDataAccess.test.ts +87 -42
- package/src/hooks/useSecureDataAccess.ts +95 -48
- package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
- 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 +8 -3
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +74 -12
- package/src/rbac/utils/contextValidator.ts +288 -0
- package/src/rbac/utils/eventContext.ts +52 -3
- package/src/services/AuthService.ts +37 -8
- package/src/services/EventService.ts +165 -21
- package/src/services/OrganisationService.ts +125 -137
- package/src/services/__tests__/EventService.test.ts +26 -21
- package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
- package/src/services/__tests__/OrganisationService.test.ts +218 -86
- package/src/types/database.generated.ts +166 -201
- package/src/types/file-reference.ts +13 -10
- package/src/types/supabase.ts +2 -2
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
- package/src/utils/app/appNameResolver.test.ts +346 -73
- package/src/utils/context/superAdminOverride.ts +58 -0
- package/src/utils/file-reference/index.ts +65 -37
- package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
- package/src/utils/google-places/googlePlacesUtils.ts +1 -1
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
- package/src/utils/google-places/types.ts +1 -1
- package/src/utils/request-deduplication.ts +4 -4
- package/src/utils/security/secureDataAccess.test.ts +1 -1
- package/src/utils/security/secureDataAccess.ts +7 -4
- package/src/utils/storage/README.md +1 -1
- 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-F2IMUDXZ.js.map +0 -1
- package/dist/chunk-HEHYGYOX.js.map +0 -1
- package/dist/chunk-IM4QE42D.js.map +0 -1
- package/dist/chunk-MX64ZF6I.js.map +0 -1
- package/dist/chunk-SAUPYVLF.js.map +0 -1
- package/dist/chunk-THRPYOFK.js.map +0 -1
- package/dist/chunk-UCQSRW7Z.js.map +0 -1
- package/dist/chunk-VGZZXKBR.js.map +0 -1
- package/dist/chunk-YGPFYGA6.js.map +0 -1
- package/dist/chunk-YHCN776L.js.map +0 -1
- /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-WKRZD47S.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
- /package/dist/{api-YP7XD5L6.js.map → api-IHKALJZD.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);
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
* - Secure organisation data handling
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
-
import React, { useState, useCallback } from 'react';
|
|
56
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
57
57
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../Select';
|
|
58
58
|
import { Alert, AlertDescription } from '../Alert/Alert';
|
|
59
59
|
import { Button } from '../Button/Button';
|
|
@@ -118,6 +118,9 @@ export function OrganisationSelector({
|
|
|
118
118
|
refreshOrganisations
|
|
119
119
|
} = useOrganisations();
|
|
120
120
|
|
|
121
|
+
// Removed debug logging useEffect - it was causing render loops because organisations array
|
|
122
|
+
// is recreated on every render, triggering the effect constantly
|
|
123
|
+
|
|
121
124
|
|
|
122
125
|
const handleOrganisationChange = useCallback(async (orgId: string) => {
|
|
123
126
|
if (disabled || isLoading) return;
|
|
@@ -184,7 +187,7 @@ export function OrganisationSelector({
|
|
|
184
187
|
return (
|
|
185
188
|
<div className={`space-y-2 ${className}`}>
|
|
186
189
|
<Alert variant="destructive">
|
|
187
|
-
<AlertCircle className="
|
|
190
|
+
<AlertCircle className="size-4" />
|
|
188
191
|
<AlertDescription>
|
|
189
192
|
Failed to load organisations: {orgError.message}
|
|
190
193
|
</AlertDescription>
|
|
@@ -197,7 +200,7 @@ export function OrganisationSelector({
|
|
|
197
200
|
disabled={isLoading}
|
|
198
201
|
className="w-full"
|
|
199
202
|
>
|
|
200
|
-
<RefreshCw className={`
|
|
203
|
+
<RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
|
201
204
|
Retry
|
|
202
205
|
</Button>
|
|
203
206
|
)}
|
|
@@ -211,7 +214,7 @@ export function OrganisationSelector({
|
|
|
211
214
|
return (
|
|
212
215
|
<div className={`space-y-2 ${className}`}>
|
|
213
216
|
<Alert>
|
|
214
|
-
<Building2 className="
|
|
217
|
+
<Building2 className="size-4" />
|
|
215
218
|
<AlertDescription>
|
|
216
219
|
No organisations available. Please contact your administrator to be added to an organisation.
|
|
217
220
|
</AlertDescription>
|
|
@@ -224,7 +227,7 @@ export function OrganisationSelector({
|
|
|
224
227
|
disabled={isLoading}
|
|
225
228
|
className="w-full"
|
|
226
229
|
>
|
|
227
|
-
<RefreshCw className={`
|
|
230
|
+
<RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
|
228
231
|
Check Again
|
|
229
232
|
</Button>
|
|
230
233
|
)}
|
|
@@ -237,28 +240,42 @@ export function OrganisationSelector({
|
|
|
237
240
|
// Switch error display
|
|
238
241
|
const switchErrorDisplay = switchError && (
|
|
239
242
|
<Alert variant="destructive" className="mt-2">
|
|
240
|
-
<AlertCircle className="
|
|
243
|
+
<AlertCircle className="size-4" />
|
|
241
244
|
<AlertDescription>{switchError}</AlertDescription>
|
|
242
245
|
</Alert>
|
|
243
246
|
);
|
|
244
247
|
|
|
245
|
-
// Normal selector state -
|
|
248
|
+
// Normal selector state - allow opening even if no organisation is selected
|
|
249
|
+
const isSelectDisabled = disabled || isLoading;
|
|
250
|
+
|
|
251
|
+
// Memoize the value to prevent render loops
|
|
252
|
+
const selectValue = useMemo(() => {
|
|
253
|
+
return selectedOrganisation?.id || '';
|
|
254
|
+
}, [selectedOrganisation?.id]);
|
|
255
|
+
|
|
246
256
|
return (
|
|
247
|
-
<div className={
|
|
257
|
+
<div className={className}>
|
|
248
258
|
<Select
|
|
249
|
-
value={
|
|
259
|
+
value={selectValue}
|
|
250
260
|
onValueChange={handleOrganisationChange}
|
|
251
|
-
disabled={
|
|
261
|
+
disabled={isSelectDisabled}
|
|
252
262
|
>
|
|
253
|
-
<SelectTrigger
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
<SelectTrigger
|
|
264
|
+
className="text-left"
|
|
265
|
+
variant="outline"
|
|
266
|
+
>
|
|
267
|
+
<SelectValue placeholder={placeholder}>
|
|
268
|
+
{selectedOrganisation && (
|
|
269
|
+
<div className="flex items-center gap-2">
|
|
270
|
+
{isLoading ? (
|
|
271
|
+
<LoadingSpinner size="sm" />
|
|
272
|
+
) : (
|
|
273
|
+
<Building2 className="size-4 flex-shrink-0" />
|
|
274
|
+
)}
|
|
275
|
+
<span className="truncate">{selectedOrganisation.display_name}</span>
|
|
276
|
+
</div>
|
|
259
277
|
)}
|
|
260
|
-
|
|
261
|
-
</div>
|
|
278
|
+
</SelectValue>
|
|
262
279
|
</SelectTrigger>
|
|
263
280
|
<SelectContent>
|
|
264
281
|
{organisations.map((org) => {
|
|
@@ -274,7 +291,7 @@ export function OrganisationSelector({
|
|
|
274
291
|
>
|
|
275
292
|
<div className="flex items-center justify-between w-full">
|
|
276
293
|
<div className="flex items-center gap-2">
|
|
277
|
-
<Building2 className="
|
|
294
|
+
<Building2 className="size-4" />
|
|
278
295
|
<div className="flex flex-col">
|
|
279
296
|
<span className="font-medium">{org.display_name}</span>
|
|
280
297
|
{!compact && org.description && (
|
|
@@ -286,7 +303,7 @@ export function OrganisationSelector({
|
|
|
286
303
|
</div>
|
|
287
304
|
{showRole && (
|
|
288
305
|
<div className="flex items-center gap-1 ml-4">
|
|
289
|
-
<Shield className="
|
|
306
|
+
<Shield className="size-3 text-muted-foreground" />
|
|
290
307
|
<span className="text-xs text-muted-foreground capitalize">
|
|
291
308
|
{userRole?.replace('_', ' ') || 'No Role'}
|
|
292
309
|
</span>
|
|
@@ -298,8 +315,11 @@ export function OrganisationSelector({
|
|
|
298
315
|
})}
|
|
299
316
|
</SelectContent>
|
|
300
317
|
</Select>
|
|
301
|
-
|
|
302
|
-
|
|
318
|
+
{switchErrorDisplay && (
|
|
319
|
+
<div className="mt-2">
|
|
320
|
+
{switchErrorDisplay}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
303
323
|
</div>
|
|
304
324
|
);
|
|
305
325
|
}
|
|
@@ -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(),
|