@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.
Files changed (438) hide show
  1. package/core-usage-manifest.json +0 -4
  2. package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
  3. package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
  4. package/dist/{DataTable-GUFUNZ3N.js → DataTable-WKRZD47S.js} +8 -8
  5. package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +84 -25
  6. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  7. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-FTSG5XH7.js} +4 -2
  8. package/dist/{api-YP7XD5L6.js → api-IHKALJZD.js} +4 -2
  9. package/dist/{chunk-VGZZXKBR.js → chunk-6LTQQAT6.js} +351 -157
  10. package/dist/chunk-6LTQQAT6.js.map +1 -0
  11. package/dist/{chunk-MX64ZF6I.js → chunk-6TQDD426.js} +15 -15
  12. package/dist/chunk-6TQDD426.js.map +1 -0
  13. package/dist/{chunk-YHCN776L.js → chunk-G37KK66H.js} +2 -75
  14. package/dist/chunk-G37KK66H.js.map +1 -0
  15. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  16. package/dist/chunk-HW3OVDUF.js.map +1 -0
  17. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  18. package/dist/chunk-I7PSE6JW.js.map +1 -0
  19. package/dist/{chunk-IM4QE42D.js → chunk-LOMZXPSN.js} +141 -326
  20. package/dist/chunk-LOMZXPSN.js.map +1 -0
  21. package/dist/chunk-OETXORNB.js +614 -0
  22. package/dist/chunk-OETXORNB.js.map +1 -0
  23. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  24. package/dist/{chunk-HEHYGYOX.js → chunk-ROXMHMY2.js} +403 -46
  25. package/dist/chunk-ROXMHMY2.js.map +1 -0
  26. package/dist/{chunk-2UUZZJFT.js → chunk-ULHIJK66.js} +228 -177
  27. package/dist/{chunk-2UUZZJFT.js.map → chunk-ULHIJK66.js.map} +1 -1
  28. package/dist/{chunk-YGPFYGA6.js → chunk-VKB2CO4Z.js} +838 -503
  29. package/dist/chunk-VKB2CO4Z.js.map +1 -0
  30. package/dist/{chunk-3GOZZZYH.js → chunk-VRGWKHDB.js} +238 -301
  31. package/dist/chunk-VRGWKHDB.js.map +1 -0
  32. package/dist/{chunk-UCQSRW7Z.js → chunk-XNYQOL3Z.js} +431 -384
  33. package/dist/chunk-XNYQOL3Z.js.map +1 -0
  34. package/dist/{chunk-DDM4CCYT.js → chunk-XYXSXPUK.js} +79 -59
  35. package/dist/chunk-XYXSXPUK.js.map +1 -0
  36. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  37. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  38. package/dist/components.d.ts +5 -6
  39. package/dist/components.js +19 -19
  40. package/dist/components.js.map +1 -1
  41. package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
  42. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  43. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  44. package/dist/hooks.d.ts +20 -15
  45. package/dist/hooks.js +14 -8
  46. package/dist/hooks.js.map +1 -1
  47. package/dist/index.d.ts +17 -15
  48. package/dist/index.js +86 -81
  49. package/dist/index.js.map +1 -1
  50. package/dist/providers.d.ts +3 -3
  51. package/dist/providers.js +3 -1
  52. package/dist/rbac/index.d.ts +77 -13
  53. package/dist/rbac/index.js +12 -9
  54. package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
  55. package/dist/types.d.ts +3 -3
  56. package/dist/types.js +1 -1
  57. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +17 -10
  58. package/dist/utils.d.ts +8 -8
  59. package/dist/utils.js +16 -16
  60. package/docs/README.md +2 -2
  61. package/docs/api/classes/ColumnFactory.md +1 -1
  62. package/docs/api/classes/ErrorBoundary.md +1 -1
  63. package/docs/api/classes/InvalidScopeError.md +2 -2
  64. package/docs/api/classes/Logger.md +1 -1
  65. package/docs/api/classes/MissingUserContextError.md +2 -2
  66. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  67. package/docs/api/classes/PermissionDeniedError.md +1 -1
  68. package/docs/api/classes/RBACAuditManager.md +2 -2
  69. package/docs/api/classes/RBACCache.md +1 -1
  70. package/docs/api/classes/RBACEngine.md +5 -5
  71. package/docs/api/classes/RBACError.md +1 -1
  72. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  73. package/docs/api/classes/SecureSupabaseClient.md +25 -20
  74. package/docs/api/classes/StorageUtils.md +7 -4
  75. package/docs/api/enums/FileCategory.md +1 -1
  76. package/docs/api/enums/LogLevel.md +1 -1
  77. package/docs/api/enums/RBACErrorCode.md +1 -1
  78. package/docs/api/enums/RPCFunction.md +1 -1
  79. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  80. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  81. package/docs/api/interfaces/AggregateConfig.md +1 -1
  82. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  83. package/docs/api/interfaces/AvatarProps.md +1 -1
  84. package/docs/api/interfaces/BadgeProps.md +1 -1
  85. package/docs/api/interfaces/ButtonProps.md +1 -1
  86. package/docs/api/interfaces/CalendarProps.md +20 -6
  87. package/docs/api/interfaces/CardProps.md +1 -1
  88. package/docs/api/interfaces/ColorPalette.md +1 -1
  89. package/docs/api/interfaces/ColorShade.md +1 -1
  90. package/docs/api/interfaces/ComplianceResult.md +1 -1
  91. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  92. package/docs/api/interfaces/DataRecord.md +1 -1
  93. package/docs/api/interfaces/DataTableAction.md +1 -1
  94. package/docs/api/interfaces/DataTableColumn.md +1 -1
  95. package/docs/api/interfaces/DataTableProps.md +1 -1
  96. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  97. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  98. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  99. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  100. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  101. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  102. package/docs/api/interfaces/ExportColumn.md +1 -1
  103. package/docs/api/interfaces/ExportOptions.md +1 -1
  104. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  105. package/docs/api/interfaces/FileMetadata.md +1 -1
  106. package/docs/api/interfaces/FileReference.md +2 -2
  107. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  108. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  109. package/docs/api/interfaces/FileUploadProps.md +30 -19
  110. package/docs/api/interfaces/FooterProps.md +1 -1
  111. package/docs/api/interfaces/FormFieldProps.md +1 -1
  112. package/docs/api/interfaces/FormProps.md +1 -1
  113. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  114. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  115. package/docs/api/interfaces/InputProps.md +1 -1
  116. package/docs/api/interfaces/LabelProps.md +1 -1
  117. package/docs/api/interfaces/LoggerConfig.md +1 -1
  118. package/docs/api/interfaces/LoginFormProps.md +1 -1
  119. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  120. package/docs/api/interfaces/NavigationContextType.md +9 -9
  121. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  122. package/docs/api/interfaces/NavigationItem.md +1 -1
  123. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  124. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  125. package/docs/api/interfaces/Organisation.md +1 -1
  126. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  127. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  128. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  129. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  130. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  131. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  132. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  133. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  134. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  135. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  136. package/docs/api/interfaces/PaletteData.md +1 -1
  137. package/docs/api/interfaces/ParsedAddress.md +2 -2
  138. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  139. package/docs/api/interfaces/ProgressProps.md +3 -11
  140. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  141. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  142. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  143. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  144. package/docs/api/interfaces/QuickFix.md +1 -1
  145. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  146. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  147. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  148. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  149. package/docs/api/interfaces/RBACConfig.md +2 -2
  150. package/docs/api/interfaces/RBACContext.md +1 -1
  151. package/docs/api/interfaces/RBACLogger.md +1 -1
  152. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  153. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  154. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  155. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  156. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  157. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  158. package/docs/api/interfaces/RBACResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  161. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  162. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  163. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  164. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  165. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  166. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  167. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  168. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  169. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  170. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  171. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  172. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  173. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  174. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  175. package/docs/api/interfaces/RouteConfig.md +10 -10
  176. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  177. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  178. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  179. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  180. package/docs/api/interfaces/SetupIssue.md +1 -1
  181. package/docs/api/interfaces/StorageConfig.md +4 -4
  182. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  183. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  184. package/docs/api/interfaces/StorageListOptions.md +22 -9
  185. package/docs/api/interfaces/StorageListResult.md +4 -4
  186. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  187. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  188. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  189. package/docs/api/interfaces/StyleImport.md +1 -1
  190. package/docs/api/interfaces/SwitchProps.md +1 -1
  191. package/docs/api/interfaces/TabsContentProps.md +1 -1
  192. package/docs/api/interfaces/TabsListProps.md +1 -1
  193. package/docs/api/interfaces/TabsProps.md +1 -1
  194. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  195. package/docs/api/interfaces/TextareaProps.md +1 -1
  196. package/docs/api/interfaces/ToastActionElement.md +1 -1
  197. package/docs/api/interfaces/ToastProps.md +1 -1
  198. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  199. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  200. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  201. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  202. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  203. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  205. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  207. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  208. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  209. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  210. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  211. package/docs/api/interfaces/UseResolvedScopeOptions.md +5 -5
  212. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  213. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  214. package/docs/api/interfaces/UserEventAccess.md +11 -11
  215. package/docs/api/interfaces/UserMenuProps.md +1 -1
  216. package/docs/api/interfaces/UserProfile.md +1 -1
  217. package/docs/api/modules.md +165 -106
  218. package/docs/api-reference/components.md +15 -7
  219. package/docs/api-reference/providers.md +2 -2
  220. package/docs/api-reference/rpc-functions.md +1 -0
  221. package/docs/best-practices/README.md +1 -1
  222. package/docs/best-practices/deployment.md +8 -8
  223. package/docs/getting-started/examples/README.md +2 -2
  224. package/docs/getting-started/installation-guide.md +4 -4
  225. package/docs/getting-started/quick-start.md +3 -3
  226. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  227. package/docs/migration/README.md +18 -0
  228. package/docs/migration/database-changes-december-2025.md +767 -0
  229. package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
  230. package/docs/rbac/compliance/compliance-guide.md +2 -2
  231. package/docs/rbac/event-based-apps.md +2 -2
  232. package/docs/rbac/getting-started.md +2 -2
  233. package/docs/rbac/quick-start.md +2 -2
  234. package/docs/security/README.md +4 -4
  235. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  236. package/docs/troubleshooting/README.md +2 -2
  237. package/docs/troubleshooting/migration.md +3 -3
  238. package/package.json +1 -3
  239. package/scripts/check-pace-core-compliance.cjs +1 -1
  240. package/scripts/check-pace-core-compliance.js +1 -1
  241. package/src/__tests__/fixtures/supabase.ts +301 -0
  242. package/src/__tests__/public-recipe-view.test.ts +19 -19
  243. package/src/__tests__/rls-policies.test.ts +210 -74
  244. package/src/components/AddressField/AddressField.test.tsx +42 -0
  245. package/src/components/AddressField/AddressField.tsx +71 -60
  246. package/src/components/AddressField/README.md +7 -6
  247. package/src/components/Alert/Alert.test.tsx +50 -10
  248. package/src/components/Alert/Alert.tsx +5 -3
  249. package/src/components/Avatar/Avatar.test.tsx +95 -43
  250. package/src/components/Avatar/Avatar.tsx +16 -16
  251. package/src/components/Button/Button.test.tsx +2 -1
  252. package/src/components/Button/Button.tsx +3 -3
  253. package/src/components/Calendar/Calendar.test.tsx +53 -37
  254. package/src/components/Calendar/Calendar.tsx +409 -82
  255. package/src/components/Card/Card.test.tsx +7 -4
  256. package/src/components/Card/Card.tsx +3 -6
  257. package/src/components/Checkbox/Checkbox.tsx +2 -2
  258. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  259. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  260. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  261. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  262. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  263. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  264. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  265. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  266. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  267. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  268. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  269. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  270. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  271. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  272. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  273. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  274. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  275. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  276. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  277. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  278. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  279. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  280. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  281. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  282. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  283. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  284. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  285. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  286. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  287. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  288. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  289. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  290. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  291. package/src/components/Dialog/Dialog.tsx +2 -2
  292. package/src/components/EventSelector/EventSelector.tsx +7 -7
  293. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  294. package/src/components/FileUpload/FileUpload.tsx +7 -4
  295. package/src/components/Header/Header.test.tsx +28 -0
  296. package/src/components/Header/Header.tsx +22 -9
  297. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  298. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  299. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  300. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  301. package/src/components/OrganisationSelector/OrganisationSelector.tsx +42 -22
  302. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  303. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  304. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  305. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  306. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  307. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  308. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  309. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  310. package/src/components/Progress/Progress.test.tsx +18 -19
  311. package/src/components/Progress/Progress.tsx +31 -32
  312. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  313. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  314. package/src/components/Select/Select.test.tsx +4 -1
  315. package/src/components/Select/Select.tsx +65 -20
  316. package/src/components/Switch/Switch.test.tsx +2 -1
  317. package/src/components/Switch/Switch.tsx +1 -1
  318. package/src/components/Toast/Toast.tsx +1 -1
  319. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  320. package/src/components/UserMenu/UserMenu.tsx +3 -3
  321. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  322. package/src/eslint-rules/pace-core-compliance.js +0 -2
  323. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  324. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  325. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  326. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  327. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  328. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  329. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +1 -1
  330. package/src/hooks/__tests__/usePublicEvent.test.ts +608 -0
  331. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  332. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +67 -24
  333. package/src/hooks/index.ts +1 -1
  334. package/src/hooks/public/usePublicEvent.ts +10 -10
  335. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  336. package/src/hooks/useAppConfig.ts +24 -5
  337. package/src/hooks/useFileDisplay.ts +298 -36
  338. package/src/hooks/useFileReference.ts +56 -11
  339. package/src/hooks/useFileUrl.ts +1 -1
  340. package/src/hooks/useInactivityTracker.ts +16 -7
  341. package/src/hooks/usePermissionCache.test.ts +85 -8
  342. package/src/hooks/useQueryCache.ts +27 -6
  343. package/src/hooks/useSecureDataAccess.test.ts +87 -42
  344. package/src/hooks/useSecureDataAccess.ts +95 -48
  345. package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
  346. package/src/providers/services/EventServiceProvider.tsx +37 -17
  347. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  348. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  349. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  350. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  351. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  352. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  353. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  354. package/src/rbac/api.ts +240 -36
  355. package/src/rbac/cache-invalidation.ts +21 -7
  356. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  357. package/src/rbac/components/NavigationGuard.tsx +23 -63
  358. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  359. package/src/rbac/components/NavigationProvider.tsx +13 -11
  360. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  361. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  362. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  363. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  364. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  365. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  366. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  367. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  368. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  369. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  370. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  371. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  372. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  373. package/src/rbac/engine.ts +4 -2
  374. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  375. package/src/rbac/hooks/index.ts +3 -0
  376. package/src/rbac/hooks/useCan.test.ts +101 -53
  377. package/src/rbac/hooks/usePermissions.ts +108 -41
  378. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  379. package/src/rbac/hooks/useRBAC.ts +83 -40
  380. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  381. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  382. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  383. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  384. package/src/rbac/request-deduplication.ts +1 -1
  385. package/src/rbac/secureClient.ts +72 -12
  386. package/src/rbac/security.ts +29 -23
  387. package/src/rbac/types.ts +10 -0
  388. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  389. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  390. package/src/rbac/utils/__tests__/eventContext.test.ts +8 -3
  391. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +74 -12
  392. package/src/rbac/utils/contextValidator.ts +288 -0
  393. package/src/rbac/utils/eventContext.ts +52 -3
  394. package/src/services/AuthService.ts +37 -8
  395. package/src/services/EventService.ts +165 -21
  396. package/src/services/OrganisationService.ts +125 -137
  397. package/src/services/__tests__/EventService.test.ts +26 -21
  398. package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
  399. package/src/services/__tests__/OrganisationService.test.ts +218 -86
  400. package/src/types/database.generated.ts +166 -201
  401. package/src/types/file-reference.ts +13 -10
  402. package/src/types/supabase.ts +2 -2
  403. package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
  404. package/src/utils/app/appNameResolver.test.ts +346 -73
  405. package/src/utils/context/superAdminOverride.ts +58 -0
  406. package/src/utils/file-reference/index.ts +65 -37
  407. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  408. package/src/utils/google-places/googlePlacesUtils.ts +1 -1
  409. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  410. package/src/utils/google-places/types.ts +1 -1
  411. package/src/utils/request-deduplication.ts +4 -4
  412. package/src/utils/security/secureDataAccess.test.ts +1 -1
  413. package/src/utils/security/secureDataAccess.ts +7 -4
  414. package/src/utils/storage/README.md +1 -1
  415. package/src/utils/storage/helpers.test.ts +1 -1
  416. package/src/utils/storage/helpers.ts +38 -19
  417. package/src/utils/storage/types.ts +15 -8
  418. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  419. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  420. package/src/vite-env.d.ts +2 -2
  421. package/dist/chunk-3GOZZZYH.js.map +0 -1
  422. package/dist/chunk-DDM4CCYT.js.map +0 -1
  423. package/dist/chunk-E7UAOUMY.js +0 -75
  424. package/dist/chunk-E7UAOUMY.js.map +0 -1
  425. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  426. package/dist/chunk-HEHYGYOX.js.map +0 -1
  427. package/dist/chunk-IM4QE42D.js.map +0 -1
  428. package/dist/chunk-MX64ZF6I.js.map +0 -1
  429. package/dist/chunk-SAUPYVLF.js.map +0 -1
  430. package/dist/chunk-THRPYOFK.js.map +0 -1
  431. package/dist/chunk-UCQSRW7Z.js.map +0 -1
  432. package/dist/chunk-VGZZXKBR.js.map +0 -1
  433. package/dist/chunk-YGPFYGA6.js.map +0 -1
  434. package/dist/chunk-YHCN776L.js.map +0 -1
  435. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-WKRZD47S.js.map} +0 -0
  436. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
  437. /package/dist/{api-YP7XD5L6.js.map → api-IHKALJZD.js.map} +0 -0
  438. /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: string;
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 h-8 w-8 border-b-2 border-main-500" aria-hidden="true"></div>
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 h-5 w-5 border-b-2 border-main-500"
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="h-8 w-8 shadow-md"
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="h-8 w-8 shadow-md"
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
- <OrganisationSelector
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="h-6 w-6 text-acc-600" />
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="h-5 w-5 text-acc-600" />
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
- expect(spinner).toHaveClass('w-4', 'h-4');
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
- expect(spinner).toHaveClass('w-6', 'h-6');
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
- expect(spinner).toHaveClass('w-8', 'h-8');
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
- expect(spinner).toHaveClass('w-6', 'h-6');
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', expectedClasses: ['w-4', 'h-4'] },
84
- { size: 'md', expectedClasses: ['w-6', 'h-6'] },
85
- { size: 'lg', expectedClasses: ['w-8', 'h-8'] },
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, expectedClasses }) => {
92
+ sizeTests.forEach(({ size, expectedClass }) => {
89
93
  const { unmount } = renderWithProviders(<LoadingSpinner size={size} />);
90
94
 
91
95
  const spinner = screen.getByRole('status');
92
- expectedClasses.forEach(className => {
93
- expect(spinner).toHaveClass(className);
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
- expect(spinner).toHaveClass('w-6', 'h-6');
250
- expect(spinner).not.toHaveClass('w-4', 'h-4', 'w-8', 'h-8');
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
- expect(updatedSpinner).toHaveClass('w-8', 'h-8', 'custom');
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: 'w-4 h-4',
89
- md: 'w-6 h-6',
90
- lg: 'w-8 h-8'
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
- <div 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">
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
- </div>
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="h-4 w-4" />
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={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
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="h-4 w-4" />
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={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
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="h-4 w-4" />
243
+ <AlertCircle className="size-4" />
241
244
  <AlertDescription>{switchError}</AlertDescription>
242
245
  </Alert>
243
246
  );
244
247
 
245
- // Normal selector state - with null check
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={`space-y-2 ${className}`}>
257
+ <div className={className}>
248
258
  <Select
249
- value={selectedOrganisation?.id || ''}
259
+ value={selectValue}
250
260
  onValueChange={handleOrganisationChange}
251
- disabled={disabled || isLoading || !selectedOrganisation}
261
+ disabled={isSelectDisabled}
252
262
  >
253
- <SelectTrigger className={`${isLoading ? 'opacity-50' : ''}`}>
254
- <div className="flex items-center gap-2">
255
- {isLoading ? (
256
- <LoadingSpinner size="sm" />
257
- ) : (
258
- <Building2 className="h-4 w-4 text-muted-foreground" />
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
- <SelectValue placeholder={placeholder} />
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="h-4 w-4" />
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="h-3 w-3 text-muted-foreground" />
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
- {switchErrorDisplay}
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(),