@jmruthers/pace-core 0.5.190 → 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 (249) hide show
  1. package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
  2. package/dist/{DataTable-ON3IXISJ.js → DataTable-WKRZD47S.js} +6 -6
  3. package/dist/{PublicPageProvider-C4uxosp6.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +1 -1
  4. package/dist/{UnifiedAuthProvider-X5NXANVI.js → UnifiedAuthProvider-FTSG5XH7.js} +3 -3
  5. package/dist/{api-I6UCQ5S6.js → api-IHKALJZD.js} +2 -2
  6. package/dist/{chunk-J2XXC7R5.js → chunk-6LTQQAT6.js} +77 -111
  7. package/dist/chunk-6LTQQAT6.js.map +1 -0
  8. package/dist/{chunk-STYK4OH2.js → chunk-6TQDD426.js} +10 -10
  9. package/dist/chunk-6TQDD426.js.map +1 -0
  10. package/dist/{chunk-DZWK57KZ.js → chunk-G37KK66H.js} +1 -1
  11. package/dist/{chunk-DZWK57KZ.js.map → chunk-G37KK66H.js.map} +1 -1
  12. package/dist/{chunk-73HSNNOQ.js → chunk-LOMZXPSN.js} +13 -13
  13. package/dist/{chunk-Y4BUBBHD.js → chunk-OETXORNB.js} +3 -3
  14. package/dist/{chunk-RUYZKXOD.js → chunk-ROXMHMY2.js} +5 -3
  15. package/dist/chunk-ROXMHMY2.js.map +1 -0
  16. package/dist/{chunk-SDMHPX3X.js → chunk-ULHIJK66.js} +56 -21
  17. package/dist/{chunk-SDMHPX3X.js.map → chunk-ULHIJK66.js.map} +1 -1
  18. package/dist/{chunk-VVBAW5A5.js → chunk-VKB2CO4Z.js} +46 -35
  19. package/dist/chunk-VKB2CO4Z.js.map +1 -0
  20. package/dist/{chunk-HQVPB5MZ.js → chunk-VRGWKHDB.js} +6 -6
  21. package/dist/{chunk-NIU6J6OX.js → chunk-XNYQOL3Z.js} +16 -16
  22. package/dist/chunk-XNYQOL3Z.js.map +1 -0
  23. package/dist/{chunk-4QYC5L4K.js → chunk-XYXSXPUK.js} +22 -27
  24. package/dist/chunk-XYXSXPUK.js.map +1 -0
  25. package/dist/components.d.ts +3 -3
  26. package/dist/components.js +8 -8
  27. package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
  28. package/dist/hooks.d.ts +12 -12
  29. package/dist/hooks.js +7 -7
  30. package/dist/index.d.ts +7 -7
  31. package/dist/index.js +18 -23
  32. package/dist/index.js.map +1 -1
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +1 -1
  35. package/dist/rbac/index.js +6 -6
  36. package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
  37. package/dist/types.d.ts +2 -2
  38. package/dist/{usePublicRouteParams-DxIDS4bC.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +1 -1
  39. package/dist/utils.d.ts +8 -8
  40. package/dist/utils.js +2 -2
  41. package/docs/api/classes/ColumnFactory.md +1 -1
  42. package/docs/api/classes/ErrorBoundary.md +1 -1
  43. package/docs/api/classes/InvalidScopeError.md +1 -1
  44. package/docs/api/classes/Logger.md +1 -1
  45. package/docs/api/classes/MissingUserContextError.md +1 -1
  46. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  47. package/docs/api/classes/PermissionDeniedError.md +1 -1
  48. package/docs/api/classes/RBACAuditManager.md +2 -2
  49. package/docs/api/classes/RBACCache.md +1 -1
  50. package/docs/api/classes/RBACEngine.md +2 -2
  51. package/docs/api/classes/RBACError.md +1 -1
  52. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  53. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  54. package/docs/api/classes/StorageUtils.md +1 -1
  55. package/docs/api/enums/FileCategory.md +1 -1
  56. package/docs/api/enums/LogLevel.md +1 -1
  57. package/docs/api/enums/RBACErrorCode.md +1 -1
  58. package/docs/api/enums/RPCFunction.md +1 -1
  59. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  60. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  63. package/docs/api/interfaces/AvatarProps.md +1 -1
  64. package/docs/api/interfaces/BadgeProps.md +1 -1
  65. package/docs/api/interfaces/ButtonProps.md +1 -1
  66. package/docs/api/interfaces/CalendarProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/ComplianceResult.md +1 -1
  71. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  72. package/docs/api/interfaces/DataRecord.md +1 -1
  73. package/docs/api/interfaces/DataTableAction.md +1 -1
  74. package/docs/api/interfaces/DataTableColumn.md +1 -1
  75. package/docs/api/interfaces/DataTableProps.md +1 -1
  76. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  77. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  78. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  79. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  80. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  81. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  82. package/docs/api/interfaces/ExportColumn.md +1 -1
  83. package/docs/api/interfaces/ExportOptions.md +1 -1
  84. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  85. package/docs/api/interfaces/FileMetadata.md +1 -1
  86. package/docs/api/interfaces/FileReference.md +1 -1
  87. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  88. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  89. package/docs/api/interfaces/FileUploadProps.md +1 -1
  90. package/docs/api/interfaces/FooterProps.md +1 -1
  91. package/docs/api/interfaces/FormFieldProps.md +1 -1
  92. package/docs/api/interfaces/FormProps.md +1 -1
  93. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  94. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  95. package/docs/api/interfaces/InputProps.md +1 -1
  96. package/docs/api/interfaces/LabelProps.md +1 -1
  97. package/docs/api/interfaces/LoggerConfig.md +1 -1
  98. package/docs/api/interfaces/LoginFormProps.md +1 -1
  99. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  100. package/docs/api/interfaces/NavigationContextType.md +1 -1
  101. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  102. package/docs/api/interfaces/NavigationItem.md +1 -1
  103. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  104. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  105. package/docs/api/interfaces/Organisation.md +1 -1
  106. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  107. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  108. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  109. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  110. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  111. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  112. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  113. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  114. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  115. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  116. package/docs/api/interfaces/PaletteData.md +1 -1
  117. package/docs/api/interfaces/ParsedAddress.md +2 -2
  118. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  119. package/docs/api/interfaces/ProgressProps.md +1 -1
  120. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  121. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  122. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  123. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  124. package/docs/api/interfaces/QuickFix.md +1 -1
  125. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  126. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  127. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  128. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  129. package/docs/api/interfaces/RBACConfig.md +2 -2
  130. package/docs/api/interfaces/RBACContext.md +1 -1
  131. package/docs/api/interfaces/RBACLogger.md +1 -1
  132. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  133. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  134. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  135. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  136. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  137. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  138. package/docs/api/interfaces/RBACResult.md +1 -1
  139. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  140. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  141. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  142. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  143. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  144. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  145. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  146. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  147. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  148. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  149. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  150. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  151. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  152. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  153. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  154. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  155. package/docs/api/interfaces/RouteConfig.md +1 -1
  156. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  157. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  158. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  159. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  160. package/docs/api/interfaces/SetupIssue.md +1 -1
  161. package/docs/api/interfaces/StorageConfig.md +1 -1
  162. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  163. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  164. package/docs/api/interfaces/StorageListOptions.md +1 -1
  165. package/docs/api/interfaces/StorageListResult.md +1 -1
  166. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  167. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  168. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  169. package/docs/api/interfaces/StyleImport.md +1 -1
  170. package/docs/api/interfaces/SwitchProps.md +1 -1
  171. package/docs/api/interfaces/TabsContentProps.md +1 -1
  172. package/docs/api/interfaces/TabsListProps.md +1 -1
  173. package/docs/api/interfaces/TabsProps.md +1 -1
  174. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  175. package/docs/api/interfaces/TextareaProps.md +1 -1
  176. package/docs/api/interfaces/ToastActionElement.md +1 -1
  177. package/docs/api/interfaces/ToastProps.md +1 -1
  178. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  179. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  180. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  181. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  182. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  183. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  184. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  185. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  186. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  187. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  188. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  189. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  190. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  191. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  192. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  193. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  194. package/docs/api/interfaces/UserEventAccess.md +1 -1
  195. package/docs/api/interfaces/UserMenuProps.md +1 -1
  196. package/docs/api/interfaces/UserProfile.md +1 -1
  197. package/docs/api/modules.md +16 -16
  198. package/docs/migration/README.md +18 -0
  199. package/docs/migration/database-changes-december-2025.md +767 -0
  200. package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
  201. package/package.json +1 -1
  202. package/src/__tests__/public-recipe-view.test.ts +10 -10
  203. package/src/__tests__/rls-policies.test.ts +13 -13
  204. package/src/components/AddressField/README.md +6 -6
  205. package/src/components/OrganisationSelector/OrganisationSelector.tsx +35 -15
  206. package/src/components/Select/Select.test.tsx +4 -1
  207. package/src/components/Select/Select.tsx +60 -15
  208. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +192 -0
  209. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +741 -0
  210. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +703 -0
  211. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +581 -0
  212. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +9 -8
  213. package/src/hooks/public/usePublicEvent.ts +8 -8
  214. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  215. package/src/hooks/useFileDisplay.ts +8 -9
  216. package/src/hooks/useQueryCache.ts +6 -6
  217. package/src/hooks/useSecureDataAccess.test.ts +8 -8
  218. package/src/hooks/useSecureDataAccess.ts +15 -11
  219. package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
  220. package/src/rbac/hooks/useRBAC.simple.test.ts +95 -0
  221. package/src/rbac/utils/__tests__/eventContext.test.ts +2 -2
  222. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +490 -0
  223. package/src/rbac/utils/eventContext.ts +5 -2
  224. package/src/services/AuthService.ts +37 -8
  225. package/src/services/OrganisationService.ts +92 -139
  226. package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
  227. package/src/services/__tests__/OrganisationService.test.ts +218 -86
  228. package/src/types/database.generated.ts +166 -201
  229. package/src/types/supabase.ts +2 -2
  230. package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
  231. package/src/utils/file-reference/index.ts +4 -4
  232. package/src/utils/google-places/googlePlacesUtils.ts +1 -1
  233. package/src/utils/google-places/types.ts +1 -1
  234. package/src/utils/request-deduplication.ts +4 -4
  235. package/src/utils/security/secureDataAccess.test.ts +1 -1
  236. package/src/utils/security/secureDataAccess.ts +7 -4
  237. package/src/utils/storage/README.md +1 -1
  238. package/dist/chunk-4QYC5L4K.js.map +0 -1
  239. package/dist/chunk-J2XXC7R5.js.map +0 -1
  240. package/dist/chunk-NIU6J6OX.js.map +0 -1
  241. package/dist/chunk-RUYZKXOD.js.map +0 -1
  242. package/dist/chunk-STYK4OH2.js.map +0 -1
  243. package/dist/chunk-VVBAW5A5.js.map +0 -1
  244. /package/dist/{DataTable-ON3IXISJ.js.map → DataTable-WKRZD47S.js.map} +0 -0
  245. /package/dist/{UnifiedAuthProvider-X5NXANVI.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
  246. /package/dist/{api-I6UCQ5S6.js.map → api-IHKALJZD.js.map} +0 -0
  247. /package/dist/{chunk-73HSNNOQ.js.map → chunk-LOMZXPSN.js.map} +0 -0
  248. /package/dist/{chunk-Y4BUBBHD.js.map → chunk-OETXORNB.js.map} +0 -0
  249. /package/dist/{chunk-HQVPB5MZ.js.map → chunk-VRGWKHDB.js.map} +0 -0
@@ -154,7 +154,7 @@ if (address) {
154
154
 
155
155
  ## ParsedAddress Type
156
156
 
157
- The `onChange` callback receives a `ParsedAddress` object matching the `pace_address` table structure:
157
+ The `onChange` callback receives a `ParsedAddress` object matching the `core_address` table structure:
158
158
 
159
159
  ```typescript
160
160
  interface ParsedAddress {
@@ -202,7 +202,7 @@ The component uses intelligent caching to reduce API costs:
202
202
 
203
203
  The `place_id` is a stable identifier for a location that doesn't change. It's crucial for:
204
204
 
205
- 1. **Storing in Database**: Save `place_id` in your `pace_address` table
205
+ 1. **Storing in Database**: Save `place_id` in your `core_address` table
206
206
  2. **Retrieving Later**: Use `getAddressByPlaceId()` to get full address without autocomplete
207
207
  3. **Verification**: Verify addresses haven't changed
208
208
  4. **Cost Efficiency**: Place details lookups are cheaper than autocomplete searches
@@ -215,7 +215,7 @@ The `place_id` is a stable identifier for a location that doesn't change. It's c
215
215
  apiKey={apiKey}
216
216
  onChange={async (address) => {
217
217
  // Store in database
218
- await supabase.from('pace_address').insert({
218
+ await supabase.from('core_address').insert({
219
219
  place_id: address.place_id, // Store this!
220
220
  full_address: address.full_address,
221
221
  lat: address.lat,
@@ -257,16 +257,16 @@ The component handles various error scenarios:
257
257
  - **Rate Limiting**: Informs user about quota limits
258
258
  - **Invalid Requests**: Validates input and displays errors
259
259
 
260
- ## Integration with pace_address Table
260
+ ## Integration with core_address Table
261
261
 
262
- The `ParsedAddress` type matches the `pace_address` table structure, making it easy to store results:
262
+ The `ParsedAddress` type matches the `core_address` table structure, making it easy to store results:
263
263
 
264
264
  ```tsx
265
265
  <AddressField
266
266
  apiKey={apiKey}
267
267
  onChange={async (address) => {
268
268
  const { data, error } = await supabase
269
- .from('pace_address')
269
+ .from('core_address')
270
270
  .insert({
271
271
  place_id: address.place_id,
272
272
  full_address: address.full_address,
@@ -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;
@@ -242,23 +245,37 @@ export function OrganisationSelector({
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="size-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) => {
@@ -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
  }
@@ -465,11 +465,14 @@ describe('Select Component', () => {
465
465
  await user.click(screen.getByRole('combobox'));
466
466
  expect(screen.getByRole('listbox')).toBeInTheDocument();
467
467
 
468
+ // Wait for the event listener to be added (100ms delay in implementation)
469
+ await new Promise(resolve => setTimeout(resolve, 150));
470
+
468
471
  await user.click(document.body);
469
472
 
470
473
  await waitFor(() => {
471
474
  expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
472
- });
475
+ }, { timeout: 2000 });
473
476
  });
474
477
 
475
478
  it('prevents opening when trigger is disabled', async () => {
@@ -145,7 +145,9 @@ export const useSelectState = ({
145
145
  }, [controlledValue, onValueChange, controlledOpen, onOpenChange]);
146
146
 
147
147
  const setOpen = React.useCallback((newOpen: boolean) => {
148
- if (disabled) return;
148
+ if (disabled) {
149
+ return;
150
+ }
149
151
 
150
152
  // Close all other select dropdowns when opening this one
151
153
  if (newOpen) {
@@ -210,24 +212,30 @@ export const useSelectEvents = ({ state, actions, selectRef }: UseSelectEventsPr
210
212
 
211
213
  // Handle click outside to close dropdown
212
214
  React.useEffect(() => {
215
+ if (!state.open) return;
216
+
213
217
  const handleClickOutside = (event: MouseEvent) => {
214
218
  const selectElement = selectRef.current;
215
- const clickedElement = event.target as Element;
216
- const isSelectItem = clickedElement?.closest('[data-testid="select-item"]');
217
- const isSearchInput = clickedElement?.closest('[data-testid="select-search-input"]');
218
- const isSelectContent = clickedElement?.closest('[data-testid="select-content"]');
219
+ if (!selectElement) return;
219
220
 
220
- if (state.open && selectElement && !selectElement.contains(event.target as Node) && !isSelectItem && !isSearchInput && !isSelectContent && !isSelecting) {
221
+ const target = event.target as Node;
222
+
223
+ // Close if clicking outside the select element
224
+ if (!selectElement.contains(target) && !isSelecting) {
221
225
  actions.setOpen(false);
222
226
  }
223
227
  };
224
228
 
225
- if (state.open) {
226
- document.addEventListener('mousedown', handleClickOutside);
229
+ // Add a small delay to avoid closing immediately when opening
230
+ // The delay ensures the click event that opened the dropdown has fully processed
231
+ const timeoutId = setTimeout(() => {
232
+ document.addEventListener('click', handleClickOutside, true); // Use capture phase
233
+ }, 100); // Small delay to let the opening click complete
234
+
227
235
  return () => {
228
- document.removeEventListener('mousedown', handleClickOutside);
236
+ clearTimeout(timeoutId);
237
+ document.removeEventListener('click', handleClickOutside, true);
229
238
  };
230
- }
231
239
  }, [state.open, actions, selectRef, isSelecting]);
232
240
 
233
241
  // Handle SelectItem mousedown events
@@ -588,6 +596,10 @@ export const Select = React.forwardRef<HTMLFormElement, SelectProps & UseSelectS
588
596
  className={cn("relative", className)}
589
597
  data-value={state.value}
590
598
  data-testid="select-root"
599
+ onSubmit={(e) => {
600
+ e.preventDefault();
601
+ e.stopPropagation();
602
+ }}
591
603
  >
592
604
  <SelectContext.Provider value={contextValue}>
593
605
  {children}
@@ -607,9 +619,25 @@ export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerPr
607
619
  const { open, disabled, value, actions, direction = 'down' } = useSelectContext();
608
620
  const opensUpward = direction === 'up';
609
621
 
610
- const handleClick = () => {
622
+ // Use ref to store the latest handleClick to avoid re-creating the effect
623
+ const handleClickRef = React.useRef<(e: React.MouseEvent) => void>();
624
+
625
+ const handleClick = React.useCallback((e: React.MouseEvent) => {
626
+ if (disabled) {
627
+ e.preventDefault();
628
+ e.stopPropagation();
629
+ return;
630
+ }
631
+
632
+ e.preventDefault();
633
+ e.stopPropagation();
611
634
  actions.setOpen(!open);
612
- };
635
+ }, [disabled, open, actions]);
636
+
637
+ // Update ref whenever handleClick changes
638
+ React.useEffect(() => {
639
+ handleClickRef.current = handleClick;
640
+ }, [handleClick]);
613
641
 
614
642
  const handleKeyDown = (e: React.KeyboardEvent) => {
615
643
  if (disabled) return;
@@ -690,9 +718,19 @@ export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerPr
690
718
  });
691
719
  }
692
720
 
721
+
722
+ // Simple ref forwarding
723
+ const handleRef = React.useCallback((node: HTMLButtonElement | null) => {
724
+ if (typeof ref === 'function') {
725
+ ref(node);
726
+ } else if (ref) {
727
+ (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;
728
+ }
729
+ }, [ref]);
730
+
693
731
  return (
694
732
  <Button
695
- ref={ref}
733
+ ref={handleRef}
696
734
  type="button"
697
735
  role="combobox"
698
736
  aria-expanded={open}
@@ -713,7 +751,9 @@ export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerPr
713
751
  textOverflow: 'ellipsis',
714
752
  whiteSpace: 'nowrap'
715
753
  }}
716
- onClick={handleClick}
754
+ onClick={(e) => {
755
+ handleClick(e);
756
+ }}
717
757
  onKeyDown={handleKeyDown}
718
758
  data-testid="select-trigger"
719
759
  data-value={value}
@@ -741,7 +781,12 @@ export const SelectValue = React.forwardRef<HTMLSpanElement, SelectValueProps>(
741
781
  const { selectedText } = useSelectContext();
742
782
 
743
783
  return (
744
- <span ref={ref} data-testid="select-value">
784
+ <span
785
+ ref={ref}
786
+ data-testid="select-value"
787
+ style={{ pointerEvents: 'none' }}
788
+ className="pointer-events-none"
789
+ >
745
790
  {children || (selectedText ? selectedText : placeholder)}
746
791
  </span>
747
792
  );
@@ -0,0 +1,192 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import React from 'react';
4
+ import { TestWrapper } from '../../__tests__/helpers/test-utils';
5
+ import { useRBAC } from '../../rbac/hooks/useRBAC';
6
+
7
+ // Mock the useRBAC hook
8
+ vi.mock('../../rbac/hooks/useRBAC');
9
+
10
+ // Mock isPermittedCached from RBAC API (used by usePermissionCache)
11
+ vi.mock('../../rbac/api', async () => {
12
+ const actual = await vi.importActual('../../rbac/api');
13
+ return {
14
+ ...actual,
15
+ isPermittedCached: vi.fn().mockResolvedValue(true),
16
+ };
17
+ });
18
+
19
+ // Mock logger
20
+ vi.mock('../../utils/core/logger', () => {
21
+ const mockLoggerInstance = {
22
+ debug: vi.fn(),
23
+ info: vi.fn(),
24
+ warn: vi.fn(),
25
+ error: vi.fn(),
26
+ };
27
+ return {
28
+ createLogger: vi.fn(() => mockLoggerInstance),
29
+ logger: mockLoggerInstance,
30
+ };
31
+ });
32
+
33
+ // Mock useOrganisations hook (required by usePermissionCache)
34
+ const mockOrganisationContext = {
35
+ selectedOrganisation: {
36
+ id: 'test-org-id',
37
+ name: 'Test Organisation',
38
+ display_name: 'Test Organisation',
39
+ slug: 'test-org',
40
+ description: 'Test organisation',
41
+ subscription_tier: 'basic',
42
+ settings: {},
43
+ is_active: true,
44
+ created_at: '2023-01-01T00:00:00Z',
45
+ updated_at: '2023-01-01T00:00:00Z',
46
+ },
47
+ organisations: [],
48
+ userMemberships: [],
49
+ isLoading: false,
50
+ error: null,
51
+ hasValidOrganisationContext: true,
52
+ setSelectedOrganisation: vi.fn(),
53
+ switchOrganisation: vi.fn().mockResolvedValue(undefined),
54
+ getUserRole: vi.fn().mockReturnValue('member'),
55
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
56
+ ensureOrganisationContext: vi.fn().mockReturnValue(null),
57
+ refreshOrganisations: vi.fn().mockResolvedValue(undefined),
58
+ getPrimaryOrganisation: vi.fn().mockReturnValue(null),
59
+ isOrganisationSecure: vi.fn().mockReturnValue(true),
60
+ };
61
+
62
+ vi.mock('../useOrganisations', () => ({
63
+ useOrganisations: () => mockOrganisationContext,
64
+ }));
65
+
66
+ // Mock useUnifiedAuth (required by usePermissionCache)
67
+ const mockUseUnifiedAuthFn = vi.fn(() => ({
68
+ user: { id: 'test-user-id' },
69
+ session: null,
70
+ appName: 'test-app',
71
+ selectedOrganisation: { id: 'test-org-id' },
72
+ selectedEvent: null,
73
+ supabase: {},
74
+ isLoading: false,
75
+ error: null,
76
+ }));
77
+ vi.mock('../../providers/services/UnifiedAuthProvider', () => ({
78
+ useUnifiedAuth: () => mockUseUnifiedAuthFn(),
79
+ UnifiedAuthProvider: ({ children }: { children: React.ReactNode }) => children,
80
+ }));
81
+
82
+ // Mock useEvents hook (optional - wrapped in try/catch in usePermissionCache)
83
+ vi.mock('../useEvents', () => ({
84
+ useEvents: vi.fn(() => ({
85
+ selectedEvent: { event_id: 'event-123' },
86
+ events: [],
87
+ isLoading: false,
88
+ error: null,
89
+ })),
90
+ }));
91
+
92
+ const mockUseRBAC = vi.mocked(useRBAC);
93
+
94
+ // Import after mocking
95
+ import { usePermissionCache } from '../usePermissionCache';
96
+ import { isPermittedCached } from '../../rbac/api';
97
+ const mockIsPermittedCached = vi.mocked(isPermittedCached);
98
+
99
+ describe('usePermissionCache - Simple Tests', () => {
100
+ beforeEach(() => {
101
+ vi.clearAllMocks();
102
+
103
+ // Reset isPermittedCached mock
104
+ mockIsPermittedCached.mockClear();
105
+ mockIsPermittedCached.mockResolvedValue(true);
106
+
107
+ // Mock the useRBAC hook return value
108
+ mockUseRBAC.mockReturnValue({
109
+ hasPermission: vi.fn().mockResolvedValue(true),
110
+ user: { id: 'test-user-id' }
111
+ });
112
+ });
113
+
114
+ afterEach(() => {
115
+ vi.clearAllTimers();
116
+ });
117
+
118
+ it('should render without errors', () => {
119
+ const { result } = renderHook(() => usePermissionCache(), {
120
+ wrapper: TestWrapper
121
+ });
122
+
123
+ expect(result.current).toHaveProperty('checkPermission');
124
+ expect(result.current).toHaveProperty('checkMultiplePermissions');
125
+ expect(result.current).toHaveProperty('getCachedPermissions');
126
+ expect(result.current).toHaveProperty('invalidateCache');
127
+ expect(result.current).toHaveProperty('getDebugInfo');
128
+ expect(result.current).toHaveProperty('getAuditTrail');
129
+ });
130
+
131
+ it('should check permission and return result', async () => {
132
+ mockIsPermittedCached.mockResolvedValueOnce(true);
133
+ mockUseRBAC.mockReturnValue({
134
+ hasPermission: vi.fn().mockResolvedValue(true),
135
+ user: { id: 'test-user-id' }
136
+ });
137
+
138
+ const { result } = renderHook(() => usePermissionCache(), {
139
+ wrapper: TestWrapper
140
+ });
141
+
142
+ const permission = await result.current.checkPermission('read', 'dashboard');
143
+
144
+ expect(permission).toBe(true);
145
+ expect(mockIsPermittedCached).toHaveBeenCalled();
146
+ });
147
+
148
+ it('should handle permission check errors gracefully', async () => {
149
+ const { logger } = await import('../../utils/core/logger');
150
+ mockIsPermittedCached.mockRejectedValueOnce(new Error('Database error'));
151
+
152
+ mockUseRBAC.mockReturnValue({
153
+ hasPermission: vi.fn().mockResolvedValue(true),
154
+ user: { id: 'test-user-id' }
155
+ });
156
+
157
+ const { result } = renderHook(() => usePermissionCache(), {
158
+ wrapper: TestWrapper
159
+ });
160
+
161
+ const permission = await result.current.checkPermission('read', 'dashboard');
162
+
163
+ expect(permission).toBe(false);
164
+ // Verify error was logged using logger
165
+ expect(logger.error).toHaveBeenCalled();
166
+ });
167
+
168
+ it('should provide debug information', () => {
169
+ const { result } = renderHook(() => usePermissionCache(), {
170
+ wrapper: TestWrapper
171
+ });
172
+
173
+ const debugInfo = result.current.getDebugInfo();
174
+
175
+ expect(debugInfo).toHaveProperty('cacheSize');
176
+ expect(debugInfo).toHaveProperty('cacheHits');
177
+ expect(debugInfo).toHaveProperty('cacheMisses');
178
+ expect(debugInfo).toHaveProperty('totalChecks');
179
+ expect(debugInfo).toHaveProperty('averageResponseTime');
180
+ expect(debugInfo).toHaveProperty('lastInvalidation');
181
+ });
182
+
183
+ it('should provide audit trail', () => {
184
+ const { result } = renderHook(() => usePermissionCache(), {
185
+ wrapper: TestWrapper
186
+ });
187
+
188
+ const auditTrail = result.current.getAuditTrail();
189
+
190
+ expect(Array.isArray(auditTrail)).toBe(true);
191
+ });
192
+ });