@jmruthers/pace-core 0.5.136 → 0.5.139

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 (292) hide show
  1. package/dist/{DataTable-CYOHOX3O.js → DataTable-JXFCA2BJ.js} +10 -9
  2. package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
  3. package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
  4. package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
  5. package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
  6. package/dist/chunk-4C7EXCAR.js.map +1 -0
  7. package/dist/{chunk-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
  8. package/dist/chunk-5JMOHWDI.js.map +1 -0
  9. package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
  10. package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
  11. package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
  12. package/dist/chunk-7QCC6MCP.js.map +1 -0
  13. package/dist/chunk-BJPBT3CU.js +21 -0
  14. package/dist/chunk-BJPBT3CU.js.map +1 -0
  15. package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
  16. package/dist/chunk-BOOI7GK2.js.map +1 -0
  17. package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
  18. package/dist/chunk-JISYG63F.js +70 -0
  19. package/dist/chunk-JISYG63F.js.map +1 -0
  20. package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
  21. package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
  22. package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
  23. package/dist/{chunk-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
  24. package/dist/chunk-TLT2ZR3L.js.map +1 -0
  25. package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
  26. package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
  27. package/dist/components.d.ts +1 -1
  28. package/dist/components.js +12 -11
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.js +8 -9
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +15 -14
  34. package/dist/index.js.map +1 -1
  35. package/dist/providers.js +3 -4
  36. package/dist/rbac/index.js +8 -9
  37. package/dist/schema-DTDZQe2u.d.ts +28 -0
  38. package/dist/types.d.ts +152 -3
  39. package/dist/types.js +51 -16
  40. package/dist/types.js.map +1 -1
  41. package/dist/utils.d.ts +89 -4
  42. package/dist/utils.js +214 -96
  43. package/dist/utils.js.map +1 -1
  44. package/dist/validation.d.ts +1 -343
  45. package/dist/validation.js +3 -100
  46. package/docs/api/classes/ColumnFactory.md +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +1 -1
  49. package/docs/api/classes/MissingUserContextError.md +1 -1
  50. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  51. package/docs/api/classes/PermissionDeniedError.md +1 -1
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +1 -1
  54. package/docs/api/classes/RBACCache.md +1 -1
  55. package/docs/api/classes/RBACEngine.md +1 -1
  56. package/docs/api/classes/RBACError.md +1 -1
  57. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  58. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  59. package/docs/api/classes/StorageUtils.md +1 -1
  60. package/docs/api/enums/FileCategory.md +1 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/BadgeProps.md +27 -0
  63. package/docs/api/interfaces/ButtonProps.md +1 -1
  64. package/docs/api/interfaces/CardProps.md +1 -1
  65. package/docs/api/interfaces/ColorPalette.md +1 -1
  66. package/docs/api/interfaces/ColorShade.md +1 -1
  67. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  68. package/docs/api/interfaces/DataRecord.md +1 -1
  69. package/docs/api/interfaces/DataTableAction.md +1 -1
  70. package/docs/api/interfaces/DataTableColumn.md +1 -1
  71. package/docs/api/interfaces/DataTableProps.md +1 -1
  72. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  73. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  74. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  75. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  76. package/docs/api/interfaces/EventLogoProps.md +1 -1
  77. package/docs/api/interfaces/ExportColumn.md +1 -1
  78. package/docs/api/interfaces/ExportOptions.md +1 -1
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  128. package/docs/api/interfaces/StorageConfig.md +1 -1
  129. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  130. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  131. package/docs/api/interfaces/StorageListOptions.md +1 -1
  132. package/docs/api/interfaces/StorageListResult.md +1 -1
  133. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  134. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  135. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  136. package/docs/api/interfaces/StyleImport.md +1 -1
  137. package/docs/api/interfaces/SwitchProps.md +1 -1
  138. package/docs/api/interfaces/ToastActionElement.md +1 -1
  139. package/docs/api/interfaces/ToastProps.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  141. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  143. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  145. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  147. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  148. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  150. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  151. package/docs/api/interfaces/UserEventAccess.md +1 -1
  152. package/docs/api/interfaces/UserMenuProps.md +1 -1
  153. package/docs/api/interfaces/UserProfile.md +1 -1
  154. package/docs/api/modules.md +84 -15
  155. package/docs/architecture/README.md +0 -1
  156. package/docs/styles/README.md +0 -2
  157. package/examples/RBAC/CompleteRBACExample.tsx +324 -0
  158. package/examples/RBAC/EventBasedApp.tsx +239 -0
  159. package/examples/RBAC/PermissionExample.tsx +151 -0
  160. package/examples/RBAC/index.ts +13 -0
  161. package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
  162. package/examples/public-pages/PublicEventPage.tsx +274 -0
  163. package/examples/public-pages/PublicPageApp.tsx +308 -0
  164. package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
  165. package/examples/public-pages/index.ts +14 -0
  166. package/package.json +1 -10
  167. package/src/__tests__/TEST_STANDARD.md +92 -0
  168. package/src/components/Badge/Badge.test.tsx +314 -0
  169. package/src/components/Badge/Badge.tsx +304 -0
  170. package/src/components/Badge/index.ts +3 -0
  171. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
  172. package/src/components/DataTable/__tests__/styles.test.ts +1 -1
  173. package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
  174. package/src/components/DataTable/components/DataTableBody.tsx +461 -0
  175. package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
  176. package/src/components/DataTable/components/FilterRow.tsx +9 -3
  177. package/src/components/DataTable/components/PaginationControls.tsx +1 -0
  178. package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
  179. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
  180. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
  181. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
  182. package/src/components/DataTable/core/ActionManager.ts +235 -0
  183. package/src/components/DataTable/core/ColumnManager.ts +205 -0
  184. package/src/components/DataTable/core/DataManager.ts +188 -0
  185. package/src/components/DataTable/core/DataTableContext.tsx +181 -0
  186. package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
  187. package/src/components/DataTable/core/PluginRegistry.ts +229 -0
  188. package/src/components/DataTable/core/StateManager.ts +311 -0
  189. package/src/components/DataTable/core/interfaces.ts +338 -0
  190. package/src/components/DataTable/styles.ts +27 -6
  191. package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
  192. package/src/components/DataTable/utils/columnUtils.ts +40 -0
  193. package/src/components/DataTable/utils/debugTools.ts +609 -0
  194. package/src/components/DataTable/utils/index.ts +1 -0
  195. package/src/components/Dialog/README.md +804 -0
  196. package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
  197. package/src/components/Dialog/utils/safeHtml.ts +185 -0
  198. package/src/components/Footer/Footer.test.tsx +1 -1
  199. package/src/components/Form/Form.test.tsx +1 -1
  200. package/src/components/Form/FormErrorSummary.tsx +113 -0
  201. package/src/components/Form/FormFieldset.tsx +127 -0
  202. package/src/components/Form/FormLiveRegion.tsx +198 -0
  203. package/src/components/LoginForm/LoginForm.test.tsx +1 -1
  204. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
  205. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  206. package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
  207. package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
  208. package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
  209. package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
  210. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  211. package/src/components/Select/Select.test.tsx +1 -1
  212. package/src/components/Select/Select.tsx +20 -8
  213. package/src/components/Table/__tests__/Table.test.tsx +1 -1
  214. package/src/components/index.ts +3 -0
  215. package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
  216. package/src/index.ts +4 -0
  217. package/src/rbac/hooks/useCan.test.ts +24 -0
  218. package/src/rbac/hooks/usePermissions.ts +49 -12
  219. package/src/styles/core.css +3 -0
  220. package/src/utils/appConfig.ts +47 -0
  221. package/src/utils/appIdResolver.test.ts +499 -0
  222. package/src/utils/appIdResolver.ts +130 -0
  223. package/src/utils/appNameResolver.simple.test.ts +212 -0
  224. package/src/utils/appNameResolver.test.ts +121 -0
  225. package/src/utils/appNameResolver.ts +191 -0
  226. package/src/utils/audit.ts +127 -0
  227. package/src/utils/auth-utils.ts +96 -0
  228. package/src/utils/bundleAnalysis.ts +129 -0
  229. package/src/utils/cn.ts +7 -0
  230. package/src/utils/debugLogger.ts +67 -0
  231. package/src/utils/deviceFingerprint.ts +215 -0
  232. package/src/utils/dynamicUtils.ts +105 -0
  233. package/src/utils/file-reference.test.ts +788 -0
  234. package/src/utils/file-reference.ts +519 -0
  235. package/src/utils/formatDate.test.ts +237 -0
  236. package/src/utils/formatting.ts +133 -0
  237. package/src/utils/index.ts +7 -0
  238. package/src/utils/lazyLoad.tsx +44 -0
  239. package/src/utils/logger.ts +179 -0
  240. package/src/utils/organisationContext.test.ts +322 -0
  241. package/src/utils/organisationContext.ts +153 -0
  242. package/src/utils/performanceBenchmark.ts +64 -0
  243. package/src/utils/performanceBudgets.ts +110 -0
  244. package/src/utils/permissionTypes.ts +37 -0
  245. package/src/utils/permissionUtils.test.ts +393 -0
  246. package/src/utils/permissionUtils.ts +34 -0
  247. package/src/utils/sanitization.ts +264 -0
  248. package/src/utils/schemaUtils.ts +37 -0
  249. package/src/utils/secureDataAccess.test.ts +711 -0
  250. package/src/utils/secureDataAccess.ts +377 -0
  251. package/src/utils/secureErrors.ts +79 -0
  252. package/src/utils/secureStorage.ts +244 -0
  253. package/src/utils/security.ts +156 -0
  254. package/src/utils/securityMonitor.ts +45 -0
  255. package/src/utils/sessionTracking.ts +126 -0
  256. package/src/utils/validation.ts +111 -0
  257. package/src/utils/validationUtils.ts +120 -0
  258. package/src/validation/index.ts +2 -2
  259. package/dist/chunk-444EZN6N.js.map +0 -1
  260. package/dist/chunk-APIBCTL2.js +0 -670
  261. package/dist/chunk-APIBCTL2.js.map +0 -1
  262. package/dist/chunk-HJGGOMQ6.js.map +0 -1
  263. package/dist/chunk-K2WWTH7O.js +0 -94
  264. package/dist/chunk-K2WWTH7O.js.map +0 -1
  265. package/dist/chunk-L6PGMCMD.js.map +0 -1
  266. package/dist/chunk-LMC26NLJ.js +0 -84
  267. package/dist/chunk-LMC26NLJ.js.map +0 -1
  268. package/dist/chunk-NOHEVYVX.js.map +0 -1
  269. package/dist/chunk-TVYPTYOY.js.map +0 -1
  270. package/dist/validation-8npbysjg.d.ts +0 -177
  271. /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-JXFCA2BJ.js.map} +0 -0
  272. /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
  273. /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
  274. /package/dist/{chunk-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
  275. /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
  276. /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
  277. /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
  278. /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
  279. /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
  280. /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
  281. /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
  282. /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
  283. /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
  284. /package/examples/{components → components 2}/DataTable/index.ts +0 -0
  285. /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
  286. /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
  287. /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
  288. /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
  289. /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
  290. /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
  291. /package/examples/{components → components 2}/Dialog/index.ts +0 -0
  292. /package/examples/{components → components 2}/index.ts +0 -0
@@ -299,97 +299,95 @@ describe('useFileUrl Hook', () => {
299
299
  });
300
300
 
301
301
  it.skip('handles signed URL generation failure', async () => {
302
- // SKIPPED: This test fails when run with other test files due to mock isolation issues.
303
- // The test passes when run in isolation, but when useFileDisplay.unit.test.ts runs first,
304
- // its mock setup for getSignedUrl interferes with this test's mock override.
305
- // TODO: Refactor to use vi.spyOn or improve mock isolation between test files.
302
+ // SKIPPED: This test has issues with async promise rejection handling in the test environment.
303
+ // The mock is being called correctly, but the promise rejection isn't being caught by the hook's
304
+ // error handling. This may be due to:
305
+ // - Timing issues between beforeEach mock reset and test mock setup
306
+ // - The hook's useEffect dependencies causing callback recreation
307
+ // - How vitest handles promise rejections in this specific scenario
308
+ //
309
+ // TODO: Investigate hook's async error handling and test environment setup.
310
+ // Consider manually triggering loadUrl() instead of relying on autoLoad, or using act() to wrap async operations.
311
+
306
312
  const error = new Error('Failed to generate signed URL');
307
- // Completely reset and override the mock to reject for this test
308
- // IMPORTANT: Must clear, reset, and set new implementation before renderHook
309
- vi.mocked(getSignedUrl).mockClear();
310
- vi.mocked(getSignedUrl).mockReset();
311
- // Force a new implementation that rejects - use mockRejectedValue for clarity
312
- vi.mocked(getSignedUrl).mockRejectedValue(error);
313
+
314
+ // Override both mocks to reject - use the same pattern as success test
315
+ // The hoisted mock is what the module uses, but we also update the imported one
316
+ mockGetSignedUrl.mockImplementation(() => Promise.reject(error));
317
+ (getSignedUrl as any).mockImplementation(() => Promise.reject(error));
313
318
 
314
- await act(async () => {
315
- const { result } = renderHook(() =>
316
- useFileUrl(mockPrivateFileReference, {
317
- organisation_id: 'org-123',
318
- supabase: mockSupabase,
319
- autoLoad: true
320
- })
321
- );
322
-
323
- // Wait for the error to be set - give it enough time for the async operation
324
- await waitFor(
325
- () => {
326
- expect(result.current.error).toBeInstanceOf(Error);
327
- },
328
- { timeout: 5000 }
329
- );
330
-
331
- // Verify getSignedUrl was called with correct arguments
332
- expect(getSignedUrl).toHaveBeenCalledWith(
333
- mockSupabase,
334
- mockPrivateFileReference.file_path,
335
- expect.objectContaining({
336
- appName: 'file-reference',
337
- orgId: 'org-123',
338
- expiresIn: 3600
339
- })
340
- );
341
-
342
- // Then verify loading is false - when an Error is thrown, the hook preserves the original error message
343
- expect(result.current.isLoading).toBe(false);
344
- expect(result.current.error?.message).toBe('Failed to generate signed URL');
345
- expect(result.current.url).toBe(null);
346
- });
319
+ const { result } = renderHook(() =>
320
+ useFileUrl(mockPrivateFileReference, {
321
+ organisation_id: 'org-123',
322
+ supabase: mockSupabase,
323
+ autoLoad: true
324
+ })
325
+ );
326
+
327
+ await waitFor(
328
+ () => {
329
+ expect(result.current.error).toBeInstanceOf(Error);
330
+ expect(result.current.isLoading).toBe(false);
331
+ },
332
+ { timeout: 5000 }
333
+ );
334
+
335
+ expect(mockGetSignedUrl).toHaveBeenCalledWith(
336
+ mockSupabase,
337
+ mockPrivateFileReference.file_path,
338
+ expect.objectContaining({
339
+ appName: 'file-reference',
340
+ orgId: 'org-123',
341
+ expiresIn: 3600
342
+ })
343
+ );
344
+ expect(result.current.error?.message).toBe('Failed to generate signed URL');
345
+ expect(result.current.url).toBe(null);
347
346
  });
348
347
 
349
348
  it.skip('handles null signed URL result', async () => {
350
- // SKIPPED: This test fails when run with other test files due to mock isolation issues.
351
- // The test passes when run in isolation, but when useFileDisplay.unit.test.ts runs first,
352
- // its mock setup for getSignedUrl interferes with this test's mock override.
353
- // TODO: Refactor to use vi.spyOn or improve mock isolation between test files.
354
- // Completely reset and override the mock to return null URL for this test
355
- // IMPORTANT: Must clear, reset, and set new implementation before renderHook
356
- vi.mocked(getSignedUrl).mockClear();
357
- vi.mocked(getSignedUrl).mockReset();
358
- // Force a new implementation that resolves with null - use mockResolvedValue for clarity
359
- vi.mocked(getSignedUrl).mockResolvedValue({ url: null, expiresAt: null });
349
+ // SKIPPED: This test has issues with async promise resolution handling in the test environment.
350
+ // The mock is being called correctly, but the promise resolution isn't completing properly,
351
+ // causing isLoading to remain true. This may be due to:
352
+ // - Timing issues between beforeEach mock reset and test mock setup
353
+ // - The hook's useEffect dependencies causing callback recreation
354
+ // - How vitest handles promise resolutions when mockImplementation is overridden
355
+ //
356
+ // TODO: Investigate hook's async state management and test environment setup.
357
+ // Consider manually triggering loadUrl() instead of relying on autoLoad, or using act() to wrap async operations.
358
+
359
+ // Clear and override both mocks - beforeEach sets mockImplementation, so we need to reset first
360
+ mockGetSignedUrl.mockReset();
361
+ mockGetSignedUrl.mockResolvedValue({ url: null, expiresAt: null });
362
+ (getSignedUrl as any).mockReset();
363
+ (getSignedUrl as any).mockResolvedValue({ url: null, expiresAt: null });
360
364
 
361
- await act(async () => {
362
- const { result } = renderHook(() =>
363
- useFileUrl(mockPrivateFileReference, {
364
- organisation_id: 'org-123',
365
- supabase: mockSupabase,
366
- autoLoad: true
367
- })
368
- );
369
-
370
- // Wait for loading to complete - check loading state first
371
- await waitFor(
372
- () => {
373
- expect(result.current.isLoading).toBe(false);
374
- },
375
- { timeout: 5000 }
376
- );
377
-
378
- // Verify getSignedUrl was called with correct arguments
379
- expect(getSignedUrl).toHaveBeenCalledWith(
380
- mockSupabase,
381
- mockPrivateFileReference.file_path,
382
- expect.objectContaining({
383
- appName: 'file-reference',
384
- orgId: 'org-123',
385
- expiresIn: 3600
386
- })
387
- );
388
-
389
- // URL should be null when getSignedUrl returns null
390
- expect(result.current.url).toBe(null);
391
- expect(result.current.error).toBe(null);
392
- });
365
+ const { result } = renderHook(() =>
366
+ useFileUrl(mockPrivateFileReference, {
367
+ organisation_id: 'org-123',
368
+ supabase: mockSupabase,
369
+ autoLoad: true
370
+ })
371
+ );
372
+
373
+ await waitFor(
374
+ () => {
375
+ expect(result.current.isLoading).toBe(false);
376
+ expect(result.current.url).toBe(null);
377
+ },
378
+ { timeout: 5000 }
379
+ );
380
+
381
+ expect(getSignedUrl).toHaveBeenCalledWith(
382
+ mockSupabase,
383
+ mockPrivateFileReference.file_path,
384
+ expect.objectContaining({
385
+ appName: 'file-reference',
386
+ orgId: 'org-123',
387
+ expiresIn: 3600
388
+ })
389
+ );
390
+ expect(result.current.error).toBe(null);
393
391
  });
394
392
  });
395
393
 
package/src/index.ts CHANGED
@@ -79,6 +79,9 @@ export type { LabelProps } from './components/Label/Label';
79
79
  export { Alert, AlertTitle, AlertDescription } from './components/Alert/Alert';
80
80
  export { Avatar, AvatarImage, AvatarFallback } from './components/Avatar/Avatar';
81
81
 
82
+ export { Badge } from './components/Badge/Badge';
83
+ export type { BadgeProps, BadgeVariant } from './components/Badge/Badge';
84
+
82
85
  export { Checkbox } from './components/Checkbox/Checkbox';
83
86
  export { Switch } from './components/Switch/Switch';
84
87
  export type { SwitchProps } from './components/Switch/Switch';
@@ -141,6 +144,7 @@ export {
141
144
  type AggregateConfig,
142
145
  type EmptyStateConfig,
143
146
  type GetRowId,
147
+ type DataTableFeatureConfig,
144
148
  ColumnFactory
145
149
  } from './components/DataTable';
146
150
 
@@ -679,6 +679,30 @@ describe('useCan Hook', () => {
679
679
  expect(mockIsPermitted).not.toHaveBeenCalled();
680
680
  });
681
681
  });
682
+
683
+ it('handles undefined scope gracefully', async () => {
684
+ const { result } = renderHook(() =>
685
+ useCan(mockUserId, undefined as any, mockPermission, undefined, false)
686
+ );
687
+
688
+ await waitFor(() => {
689
+ expect(result.current.isLoading).toBe(true);
690
+ expect(result.current.can).toBe(false);
691
+ expect(mockIsPermitted).not.toHaveBeenCalled();
692
+ });
693
+ });
694
+
695
+ it('handles null scope gracefully', async () => {
696
+ const { result } = renderHook(() =>
697
+ useCan(mockUserId, null as any, mockPermission, undefined, false)
698
+ );
699
+
700
+ await waitFor(() => {
701
+ expect(result.current.isLoading).toBe(true);
702
+ expect(result.current.can).toBe(false);
703
+ expect(mockIsPermitted).not.toHaveBeenCalled();
704
+ });
705
+ });
682
706
  });
683
707
 
684
708
  describe('Performance', () => {
@@ -228,9 +228,15 @@ export function useCan(
228
228
  const [isLoading, setIsLoading] = useState(true);
229
229
  const [error, setError] = useState<Error | null>(null);
230
230
 
231
+ // Validate scope parameter - handle undefined/null scope gracefully
232
+ const isValidScope = scope && typeof scope === 'object';
233
+ const organisationId = isValidScope ? scope.organisationId : undefined;
234
+ const eventId = isValidScope ? scope.eventId : undefined;
235
+ const appId = isValidScope ? scope.appId : undefined;
236
+
231
237
  // Add timeout for missing organisation context (3 seconds)
232
238
  useEffect(() => {
233
- if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
239
+ if (!isValidScope || !organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === '')) {
234
240
  const timeoutId = setTimeout(() => {
235
241
  setError(new Error('Organisation context is required for permission checks'));
236
242
  setIsLoading(false);
@@ -243,7 +249,7 @@ export function useCan(
243
249
  if (error?.message === 'Organisation context is required for permission checks') {
244
250
  setError(null);
245
251
  }
246
- }, [scope.organisationId, error]);
252
+ }, [isValidScope, organisationId, error]);
247
253
 
248
254
  // Use refs to track the last values to prevent unnecessary re-runs
249
255
  const lastUserIdRef = useRef<UUID | null>(null);
@@ -253,8 +259,8 @@ export function useCan(
253
259
  const lastUseCacheRef = useRef<boolean | null>(null);
254
260
 
255
261
  useEffect(() => {
256
- // Create a scope key to track changes
257
- const scopeKey = `${scope.organisationId}-${scope.eventId}-${scope.appId}`;
262
+ // Create a scope key to track changes - use safe property access
263
+ const scopeKey = isValidScope ? `${organisationId}-${eventId}-${appId}` : 'invalid-scope';
258
264
 
259
265
  // Only run if something has actually changed
260
266
  if (
@@ -278,9 +284,18 @@ export function useCan(
278
284
  return;
279
285
  }
280
286
 
287
+ // Validate scope before accessing properties
288
+ if (!isValidScope) {
289
+ setIsLoading(true);
290
+ setCan(false);
291
+ setError(null);
292
+ // Timeout is handled in separate useEffect
293
+ return;
294
+ }
295
+
281
296
  // Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
282
297
  // Wait for organisation context to resolve
283
- if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
298
+ if (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === '')) {
284
299
  setIsLoading(true);
285
300
  setCan(false);
286
301
  setError(null);
@@ -292,9 +307,16 @@ export function useCan(
292
307
  setIsLoading(true);
293
308
  setError(null);
294
309
 
310
+ // Create a valid scope object for the API call
311
+ const validScope: Scope = {
312
+ organisationId,
313
+ ...(eventId ? { eventId } : {}),
314
+ ...(appId ? { appId } : {})
315
+ };
316
+
295
317
  const result = useCache
296
- ? await isPermittedCached({ userId, scope, permission, pageId })
297
- : await isPermitted({ userId, scope, permission, pageId });
318
+ ? await isPermittedCached({ userId, scope: validScope, permission, pageId })
319
+ : await isPermitted({ userId, scope: validScope, permission, pageId });
298
320
 
299
321
  setCan(result);
300
322
  } catch (err) {
@@ -309,7 +331,7 @@ export function useCan(
309
331
 
310
332
  checkPermission();
311
333
  }
312
- }, [userId, scope.organisationId, scope.eventId, scope.appId, permission, pageId, useCache]);
334
+ }, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache]);
313
335
 
314
336
  const refetch = useCallback(async () => {
315
337
  if (!userId) {
@@ -318,8 +340,16 @@ export function useCan(
318
340
  return;
319
341
  }
320
342
 
343
+ // Validate scope before accessing properties
344
+ if (!isValidScope) {
345
+ setCan(false);
346
+ setIsLoading(true);
347
+ setError(null);
348
+ return;
349
+ }
350
+
321
351
  // Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
322
- if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
352
+ if (!organisationId || organisationId === null || (typeof organisationId === 'string' && organisationId.trim() === '')) {
323
353
  setCan(false);
324
354
  setIsLoading(true);
325
355
  setError(null);
@@ -330,9 +360,16 @@ export function useCan(
330
360
  setIsLoading(true);
331
361
  setError(null);
332
362
 
363
+ // Create a valid scope object for the API call
364
+ const validScope: Scope = {
365
+ organisationId,
366
+ ...(eventId ? { eventId } : {}),
367
+ ...(appId ? { appId } : {})
368
+ };
369
+
333
370
  const result = useCache
334
- ? await isPermittedCached({ userId, scope, permission, pageId })
335
- : await isPermitted({ userId, scope, permission, pageId });
371
+ ? await isPermittedCached({ userId, scope: validScope, permission, pageId })
372
+ : await isPermitted({ userId, scope: validScope, permission, pageId });
336
373
 
337
374
  setCan(result);
338
375
  } catch (err) {
@@ -341,7 +378,7 @@ export function useCan(
341
378
  } finally {
342
379
  setIsLoading(false);
343
380
  }
344
- }, [userId, scope.organisationId, scope.eventId, scope.appId, permission, pageId, useCache]);
381
+ }, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache]);
345
382
 
346
383
  // Memoize the return object to prevent unnecessary re-renders
347
384
  return useMemo(() => ({
@@ -7,6 +7,8 @@
7
7
  --font-serif: "Open Sans", sans-serif;
8
8
  --font-mono: "Reddit Mono", monospace;
9
9
  --font-heading: "Georama", sans-serif;
10
+ --shadow-badge-soft: 0 0 0 0.1rem var(--color-main-600), 0 0 0 5rem var(--color-main-600) inset, 0 0 0.25rem 0.25rem var(--color-main-600);
11
+ --shadow-badge-soft-xl: 0 0 0 0.1rem var(--color-main-600), 0 0 0 5rem var(--color-main-600) inset, 0 0 0.25rem 0.75rem var(--color-main-600);
10
12
  }
11
13
 
12
14
  @layer base {
@@ -237,6 +239,7 @@
237
239
  @layer utilities {
238
240
  /* Custom utility styles go here */
239
241
 
242
+
240
243
  /* Hide spinner arrows on number inputs in DataTable */
241
244
  .datatable-number-no-spinners::-webkit-inner-spin-button,
242
245
  .datatable-number-no-spinners::-webkit-outer-spin-button {
@@ -0,0 +1,47 @@
1
+
2
+ /**
3
+ * Application configuration utilities
4
+ */
5
+
6
+ export interface AppConfig {
7
+ appName: string;
8
+ appId: string;
9
+ }
10
+
11
+ let currentAppConfig: AppConfig | null = null;
12
+
13
+ /**
14
+ * Set the current application configuration
15
+ */
16
+ export function setAppConfig(config: AppConfig) {
17
+ currentAppConfig = config;
18
+ }
19
+
20
+ /**
21
+ * Get the current application configuration
22
+ */
23
+ export function getAppConfig(): AppConfig {
24
+ if (!currentAppConfig) {
25
+ // Fallback to environment or default
26
+ const appName = import.meta.env.REACT_APP_NAME || 'PACE';
27
+ return {
28
+ appName,
29
+ appId: appName
30
+ };
31
+ }
32
+ return currentAppConfig;
33
+ }
34
+
35
+ /**
36
+ * Get the current app name
37
+ */
38
+ export function getCurrentAppName(): string {
39
+ return getAppConfig().appName;
40
+ }
41
+
42
+ /**
43
+ * Get the current app ID
44
+ */
45
+ export function getCurrentAppId(): string {
46
+ return getAppConfig().appId;
47
+ }