@jmruthers/pace-core 0.6.2 → 0.6.4

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 (299) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/cursor-rules/00-pace-core-compliance.mdc +34 -2
  3. package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
  4. package/dist/{DataTable-TPTKCX4D.js → DataTable-E7YQZD7D.js} +9 -8
  5. package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
  6. package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
  7. package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-QPXO24B4.js} +5 -4
  8. package/dist/{api-MVVQZLJI.js → api-6LVZTHDS.js} +10 -10
  9. package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
  10. package/dist/chunk-36LVWXB2.js +227 -0
  11. package/dist/chunk-36LVWXB2.js.map +1 -0
  12. package/dist/{chunk-24UVZUZG.js → chunk-3LPHPB62.js} +129 -387
  13. package/dist/chunk-3LPHPB62.js.map +1 -0
  14. package/dist/{chunk-2UOI2FG5.js → chunk-5EC5MEWX.js} +4 -4
  15. package/dist/{chunk-3XC4CPTD.js → chunk-7JPAB3T5.js} +244 -5727
  16. package/dist/chunk-7JPAB3T5.js.map +1 -0
  17. package/dist/{chunk-6J4GEEJR.js → chunk-ATKZM7RX.js} +53 -27
  18. package/dist/chunk-ATKZM7RX.js.map +1 -0
  19. package/dist/{chunk-EHMR7VYL.js → chunk-AVMLPIM7.js} +443 -189
  20. package/dist/chunk-AVMLPIM7.js.map +1 -0
  21. package/dist/chunk-DGUM43GV.js +11 -0
  22. package/dist/{chunk-NECFR5MM.js → chunk-I6DAQMWX.js} +575 -647
  23. package/dist/chunk-I6DAQMWX.js.map +1 -0
  24. package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
  25. package/dist/{chunk-XWQCNGTQ.js → chunk-NN6WWZ5U.js} +173 -79
  26. package/dist/chunk-NN6WWZ5U.js.map +1 -0
  27. package/dist/{chunk-MMZ7JXPU.js → chunk-OEWDTMG7.js} +13 -21
  28. package/dist/{chunk-MMZ7JXPU.js.map → chunk-OEWDTMG7.js.map} +1 -1
  29. package/dist/{chunk-SFZUDBL5.js → chunk-YKRAFF5K.js} +70 -56
  30. package/dist/chunk-YKRAFF5K.js.map +1 -0
  31. package/dist/components.d.ts +2 -2
  32. package/dist/components.js +12 -13
  33. package/dist/contextValidator-OOPCLPZW.js +9 -0
  34. package/dist/contextValidator-OOPCLPZW.js.map +1 -0
  35. package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
  36. package/dist/hooks.d.ts +2 -2
  37. package/dist/hooks.js +7 -6
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +7 -7
  40. package/dist/index.js +21 -16
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.d.ts +3 -3
  43. package/dist/providers.js +4 -3
  44. package/dist/rbac/index.d.ts +67 -27
  45. package/dist/rbac/index.js +15 -8
  46. package/dist/styles/index.js +1 -1
  47. package/dist/theming/runtime.js +1 -1
  48. package/dist/types.js +1 -1
  49. package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
  50. package/dist/utils.js +5 -7
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/README.md +14 -16
  53. package/docs/api/modules.md +3796 -2513
  54. package/docs/components/context-selector.md +126 -0
  55. package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
  56. package/docs/pace-mint-fix-auto-selection.md +218 -0
  57. package/docs/pace-mint-rbac-setup.md +391 -0
  58. package/docs/rbac/secure-client-protection.md +330 -0
  59. package/package.json +10 -5
  60. package/scripts/audit/core/checks/compliance.cjs +72 -0
  61. package/scripts/audit/core/checks/dependencies.cjs +568 -28
  62. package/scripts/audit/core/checks/documentation.cjs +68 -3
  63. package/scripts/audit/core/checks/environment.cjs +2 -14
  64. package/scripts/audit/core/checks/error-handling.cjs +47 -6
  65. package/src/components/ContextSelector/ContextSelector.tsx +384 -0
  66. package/src/components/ContextSelector/index.ts +3 -0
  67. package/src/components/DataTable/components/RowComponent.tsx +19 -19
  68. package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
  69. package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
  70. package/src/components/Dialog/Dialog.tsx +29 -1
  71. package/src/components/FileDisplay/FileDisplay.tsx +42 -10
  72. package/src/components/Header/Header.test.tsx +43 -73
  73. package/src/components/Header/Header.tsx +44 -45
  74. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
  75. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
  76. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
  77. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
  78. package/src/components/PaceAppLayout/PaceAppLayout.tsx +157 -36
  79. package/src/components/PaceAppLayout/README.md +14 -17
  80. package/src/components/PaceAppLayout/test-setup.tsx +2 -2
  81. package/src/components/index.ts +5 -5
  82. package/src/eslint-rules/pace-core-compliance.cjs +106 -0
  83. package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
  84. package/src/hooks/useAppConfig.ts +15 -30
  85. package/src/hooks/useFileDisplay.ts +77 -50
  86. package/src/index.ts +4 -5
  87. package/src/providers/services/AuthServiceProvider.tsx +17 -7
  88. package/src/providers/services/EventServiceProvider.tsx +33 -5
  89. package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
  90. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
  91. package/src/rbac/adapters.tsx +2 -2
  92. package/src/rbac/api.test.ts +59 -51
  93. package/src/rbac/api.ts +178 -132
  94. package/src/rbac/components/PagePermissionGuard.tsx +38 -10
  95. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
  96. package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
  97. package/src/rbac/hooks/permissions/useCan.ts +41 -11
  98. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
  99. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
  100. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
  101. package/src/rbac/hooks/useCan.test.ts +0 -9
  102. package/src/rbac/hooks/useRBAC.test.ts +1 -5
  103. package/src/rbac/hooks/useRBAC.ts +36 -37
  104. package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
  105. package/src/rbac/hooks/useResolvedScope.ts +35 -40
  106. package/src/rbac/hooks/useSecureSupabase.ts +7 -7
  107. package/src/rbac/index.ts +7 -0
  108. package/src/rbac/secureClient.test.ts +22 -18
  109. package/src/rbac/secureClient.ts +103 -16
  110. package/src/rbac/security.ts +0 -17
  111. package/src/rbac/types.ts +1 -0
  112. package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
  113. package/src/rbac/utils/clientSecurity.ts +93 -0
  114. package/src/rbac/utils/contextValidator.ts +77 -168
  115. package/src/services/AuthService.ts +39 -7
  116. package/src/services/EventService.ts +285 -56
  117. package/src/services/OrganisationService.ts +81 -14
  118. package/src/services/__tests__/EventService.test.ts +1 -2
  119. package/src/services/base/BaseService.ts +3 -0
  120. package/src/utils/dynamic/dynamicUtils.ts +7 -4
  121. package/dist/chunk-24UVZUZG.js.map +0 -1
  122. package/dist/chunk-3XC4CPTD.js.map +0 -1
  123. package/dist/chunk-6J4GEEJR.js.map +0 -1
  124. package/dist/chunk-7D4SUZUM.js +0 -38
  125. package/dist/chunk-EHMR7VYL.js.map +0 -1
  126. package/dist/chunk-NECFR5MM.js.map +0 -1
  127. package/dist/chunk-SFZUDBL5.js.map +0 -1
  128. package/dist/chunk-XWQCNGTQ.js.map +0 -1
  129. package/docs/api/classes/ColumnFactory.md +0 -243
  130. package/docs/api/classes/InvalidScopeError.md +0 -73
  131. package/docs/api/classes/Logger.md +0 -178
  132. package/docs/api/classes/MissingUserContextError.md +0 -66
  133. package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
  134. package/docs/api/classes/PermissionDeniedError.md +0 -73
  135. package/docs/api/classes/RBACAuditManager.md +0 -297
  136. package/docs/api/classes/RBACCache.md +0 -322
  137. package/docs/api/classes/RBACEngine.md +0 -171
  138. package/docs/api/classes/RBACError.md +0 -76
  139. package/docs/api/classes/RBACNotInitializedError.md +0 -66
  140. package/docs/api/classes/SecureSupabaseClient.md +0 -163
  141. package/docs/api/classes/StorageUtils.md +0 -328
  142. package/docs/api/enums/FileCategory.md +0 -184
  143. package/docs/api/enums/LogLevel.md +0 -54
  144. package/docs/api/enums/RBACErrorCode.md +0 -228
  145. package/docs/api/enums/RPCFunction.md +0 -118
  146. package/docs/api/interfaces/AddressFieldProps.md +0 -241
  147. package/docs/api/interfaces/AddressFieldRef.md +0 -94
  148. package/docs/api/interfaces/AggregateConfig.md +0 -43
  149. package/docs/api/interfaces/AutocompleteOptions.md +0 -75
  150. package/docs/api/interfaces/AvatarProps.md +0 -128
  151. package/docs/api/interfaces/BadgeProps.md +0 -34
  152. package/docs/api/interfaces/ButtonProps.md +0 -56
  153. package/docs/api/interfaces/CalendarProps.md +0 -73
  154. package/docs/api/interfaces/CardProps.md +0 -69
  155. package/docs/api/interfaces/ColorPalette.md +0 -7
  156. package/docs/api/interfaces/ColorShade.md +0 -66
  157. package/docs/api/interfaces/ComplianceResult.md +0 -30
  158. package/docs/api/interfaces/DataAccessRecord.md +0 -96
  159. package/docs/api/interfaces/DataRecord.md +0 -11
  160. package/docs/api/interfaces/DataTableAction.md +0 -252
  161. package/docs/api/interfaces/DataTableColumn.md +0 -504
  162. package/docs/api/interfaces/DataTableProps.md +0 -625
  163. package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
  164. package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
  165. package/docs/api/interfaces/DatabaseIssue.md +0 -41
  166. package/docs/api/interfaces/EmptyStateConfig.md +0 -61
  167. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
  168. package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
  169. package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
  170. package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
  171. package/docs/api/interfaces/EventAppRoleData.md +0 -71
  172. package/docs/api/interfaces/ExportColumn.md +0 -90
  173. package/docs/api/interfaces/ExportOptions.md +0 -126
  174. package/docs/api/interfaces/FileDisplayProps.md +0 -249
  175. package/docs/api/interfaces/FileMetadata.md +0 -129
  176. package/docs/api/interfaces/FileReference.md +0 -118
  177. package/docs/api/interfaces/FileSizeLimits.md +0 -7
  178. package/docs/api/interfaces/FileUploadOptions.md +0 -139
  179. package/docs/api/interfaces/FileUploadProps.md +0 -296
  180. package/docs/api/interfaces/FooterProps.md +0 -107
  181. package/docs/api/interfaces/FormFieldProps.md +0 -166
  182. package/docs/api/interfaces/FormProps.md +0 -113
  183. package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
  184. package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
  185. package/docs/api/interfaces/InputProps.md +0 -56
  186. package/docs/api/interfaces/LabelProps.md +0 -107
  187. package/docs/api/interfaces/LoggerConfig.md +0 -62
  188. package/docs/api/interfaces/LoginFormProps.md +0 -187
  189. package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
  190. package/docs/api/interfaces/NavigationContextType.md +0 -164
  191. package/docs/api/interfaces/NavigationGuardProps.md +0 -139
  192. package/docs/api/interfaces/NavigationItem.md +0 -120
  193. package/docs/api/interfaces/NavigationMenuProps.md +0 -221
  194. package/docs/api/interfaces/NavigationProviderProps.md +0 -117
  195. package/docs/api/interfaces/Organisation.md +0 -140
  196. package/docs/api/interfaces/OrganisationContextType.md +0 -388
  197. package/docs/api/interfaces/OrganisationMembership.md +0 -140
  198. package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
  199. package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
  200. package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
  201. package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
  202. package/docs/api/interfaces/PageAccessRecord.md +0 -85
  203. package/docs/api/interfaces/PagePermissionContextType.md +0 -140
  204. package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
  205. package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
  206. package/docs/api/interfaces/PaletteData.md +0 -41
  207. package/docs/api/interfaces/ParsedAddress.md +0 -120
  208. package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
  209. package/docs/api/interfaces/ProgressProps.md +0 -42
  210. package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
  211. package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
  212. package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
  213. package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
  214. package/docs/api/interfaces/QuickFix.md +0 -52
  215. package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
  216. package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
  217. package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
  218. package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
  219. package/docs/api/interfaces/RBACConfig.md +0 -133
  220. package/docs/api/interfaces/RBACContext.md +0 -52
  221. package/docs/api/interfaces/RBACLogger.md +0 -112
  222. package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
  223. package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
  224. package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
  225. package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
  226. package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
  227. package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
  228. package/docs/api/interfaces/RBACResult.md +0 -58
  229. package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
  230. package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
  231. package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
  232. package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
  233. package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
  234. package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
  235. package/docs/api/interfaces/RBACRolesListParams.md +0 -52
  236. package/docs/api/interfaces/RBACRolesListResult.md +0 -74
  237. package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
  238. package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
  239. package/docs/api/interfaces/ResourcePermissions.md +0 -155
  240. package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
  241. package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
  242. package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
  243. package/docs/api/interfaces/RoleManagementResult.md +0 -52
  244. package/docs/api/interfaces/RouteAccessRecord.md +0 -107
  245. package/docs/api/interfaces/RouteConfig.md +0 -134
  246. package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
  247. package/docs/api/interfaces/SecureDataContextType.md +0 -168
  248. package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
  249. package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
  250. package/docs/api/interfaces/SetupIssue.md +0 -41
  251. package/docs/api/interfaces/StorageConfig.md +0 -41
  252. package/docs/api/interfaces/StorageFileInfo.md +0 -74
  253. package/docs/api/interfaces/StorageFileMetadata.md +0 -151
  254. package/docs/api/interfaces/StorageListOptions.md +0 -99
  255. package/docs/api/interfaces/StorageListResult.md +0 -41
  256. package/docs/api/interfaces/StorageUploadOptions.md +0 -101
  257. package/docs/api/interfaces/StorageUploadResult.md +0 -63
  258. package/docs/api/interfaces/StorageUrlOptions.md +0 -60
  259. package/docs/api/interfaces/StyleImport.md +0 -19
  260. package/docs/api/interfaces/SwitchProps.md +0 -34
  261. package/docs/api/interfaces/TabsContentProps.md +0 -9
  262. package/docs/api/interfaces/TabsListProps.md +0 -9
  263. package/docs/api/interfaces/TabsProps.md +0 -9
  264. package/docs/api/interfaces/TabsTriggerProps.md +0 -50
  265. package/docs/api/interfaces/TextareaProps.md +0 -53
  266. package/docs/api/interfaces/ToastActionElement.md +0 -12
  267. package/docs/api/interfaces/ToastProps.md +0 -9
  268. package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
  269. package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
  270. package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
  271. package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
  272. package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
  273. package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
  274. package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
  275. package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
  276. package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
  277. package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
  278. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
  279. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
  280. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
  281. package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
  282. package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
  283. package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
  284. package/docs/api/interfaces/UserEventAccess.md +0 -121
  285. package/docs/api/interfaces/UserMenuProps.md +0 -88
  286. package/docs/api/interfaces/UserProfile.md +0 -63
  287. package/src/components/EventSelector/EventSelector.test.tsx +0 -720
  288. package/src/components/EventSelector/EventSelector.tsx +0 -423
  289. package/src/components/EventSelector/index.ts +0 -3
  290. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
  291. package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
  292. package/src/components/OrganisationSelector/index.ts +0 -9
  293. /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-E7YQZD7D.js.map} +0 -0
  294. /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-QPXO24B4.js.map} +0 -0
  295. /package/dist/{api-MVVQZLJI.js.map → api-6LVZTHDS.js.map} +0 -0
  296. /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
  297. /package/dist/{chunk-2UOI2FG5.js.map → chunk-5EC5MEWX.js.map} +0 -0
  298. /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
  299. /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
@@ -0,0 +1,218 @@
1
+ # Fix for Auto-Selection Issue in pace-mint AppLayout
2
+
3
+ ## Problem
4
+
5
+ When a user switches from an event to an organisation in the context selector, pace-mint's AppLayout component is auto-selecting the event again, even though the user explicitly cleared it. This causes the selector to show the event instead of the organisation.
6
+
7
+ ## Root Cause
8
+
9
+ pace-mint's AppLayout has its own auto-selection logic that runs whenever:
10
+ - Events are loaded/refreshed
11
+ - `selectedEvent` becomes `null`
12
+ - There's only one event available
13
+
14
+ This logic doesn't distinguish between:
15
+ - **Initial load** (should auto-select)
16
+ - **User explicitly cleared event** (should NOT auto-select)
17
+
18
+ ## Solution
19
+
20
+ Update pace-mint's AppLayout component to track when the user explicitly clears the event selection, and skip auto-selection in those cases.
21
+
22
+ ## Implementation Steps
23
+
24
+ ### Step 1: Track Previous Event Selection
25
+
26
+ Add a ref to track the previous `selectedEvent` value to detect when the user explicitly clears it:
27
+
28
+ ```tsx
29
+ import { useRef, useEffect } from 'react';
30
+ import { useEvents } from '@jmruthers/pace-core';
31
+
32
+ function AppLayout() {
33
+ const { events, selectedEvent, setSelectedEvent } = useEvents();
34
+
35
+ // Track previous selectedEvent to detect explicit clears
36
+ const previousSelectedEventRef = useRef<Event | null>(null);
37
+ const userExplicitlyClearedRef = useRef<boolean>(false);
38
+
39
+ // Detect when user explicitly clears event (was set, now null)
40
+ useEffect(() => {
41
+ const wasSet = previousSelectedEventRef.current !== null;
42
+ const isNowNull = selectedEvent === null;
43
+
44
+ if (wasSet && isNowNull) {
45
+ // User explicitly cleared the event
46
+ userExplicitlyClearedRef.current = true;
47
+ } else if (selectedEvent !== null) {
48
+ // Event is selected, reset the flag
49
+ userExplicitlyClearedRef.current = false;
50
+ }
51
+
52
+ previousSelectedEventRef.current = selectedEvent;
53
+ }, [selectedEvent]);
54
+
55
+ // ... rest of component
56
+ }
57
+ ```
58
+
59
+ ### Step 2: Update Auto-Selection Logic
60
+
61
+ Modify your auto-selection logic to check the flag before auto-selecting:
62
+
63
+ ```tsx
64
+ // OLD CODE (problematic):
65
+ useEffect(() => {
66
+ if (!selectedEvent && events.length === 1 && !isLoading) {
67
+ // Auto-select single event
68
+ setSelectedEvent(events[0]);
69
+ }
70
+ }, [selectedEvent, events, isLoading, setSelectedEvent]);
71
+
72
+ // NEW CODE (fixed):
73
+ useEffect(() => {
74
+ // Only auto-select if:
75
+ // 1. No event is currently selected
76
+ // 2. There's exactly one event available
77
+ // 3. Events are not loading
78
+ // 4. User did NOT explicitly clear the event
79
+ if (
80
+ !selectedEvent &&
81
+ events.length === 1 &&
82
+ !isLoading &&
83
+ !userExplicitlyClearedRef.current
84
+ ) {
85
+ // Auto-select single event
86
+ setSelectedEvent(events[0]);
87
+ }
88
+ }, [selectedEvent, events, isLoading, setSelectedEvent]);
89
+ ```
90
+
91
+ ### Step 3: Reset Flag on Initial Load (Optional)
92
+
93
+ If you want to allow auto-selection on initial page load but prevent it after user actions, you can track whether this is the initial load:
94
+
95
+ ```tsx
96
+ const isInitialLoadRef = useRef<boolean>(true);
97
+
98
+ useEffect(() => {
99
+ // After first render, mark initial load as complete
100
+ if (isInitialLoadRef.current && events.length > 0) {
101
+ isInitialLoadRef.current = false;
102
+ }
103
+ }, [events]);
104
+
105
+ // Then in auto-selection logic:
106
+ useEffect(() => {
107
+ if (
108
+ !selectedEvent &&
109
+ events.length === 1 &&
110
+ !isLoading &&
111
+ (!userExplicitlyClearedRef.current || isInitialLoadRef.current)
112
+ ) {
113
+ setSelectedEvent(events[0]);
114
+ }
115
+ }, [selectedEvent, events, isLoading, setSelectedEvent]);
116
+ ```
117
+
118
+ ## Complete Example
119
+
120
+ Here's a complete example of the fix:
121
+
122
+ ```tsx
123
+ import { useRef, useEffect } from 'react';
124
+ import { useEvents } from '@jmruthers/pace-core';
125
+ import type { Event } from '@jmruthers/pace-core';
126
+
127
+ function AppLayout() {
128
+ const { events, selectedEvent, setSelectedEvent, isLoading } = useEvents();
129
+
130
+ // Track previous selectedEvent to detect explicit clears
131
+ const previousSelectedEventRef = useRef<Event | null>(null);
132
+ const userExplicitlyClearedRef = useRef<boolean>(false);
133
+ const isInitialLoadRef = useRef<boolean>(true);
134
+
135
+ // Detect when user explicitly clears event
136
+ useEffect(() => {
137
+ const wasSet = previousSelectedEventRef.current !== null;
138
+ const isNowNull = selectedEvent === null;
139
+
140
+ if (wasSet && isNowNull) {
141
+ // User explicitly cleared the event (e.g., switched to organisation)
142
+ userExplicitlyClearedRef.current = true;
143
+ console.log('[AppLayout] User explicitly cleared event - preventing auto-selection');
144
+ } else if (selectedEvent !== null) {
145
+ // Event is selected, reset the flag
146
+ userExplicitlyClearedRef.current = false;
147
+ }
148
+
149
+ previousSelectedEventRef.current = selectedEvent;
150
+ }, [selectedEvent]);
151
+
152
+ // Mark initial load as complete after events are loaded
153
+ useEffect(() => {
154
+ if (isInitialLoadRef.current && events.length > 0 && !isLoading) {
155
+ isInitialLoadRef.current = false;
156
+ }
157
+ }, [events, isLoading]);
158
+
159
+ // Auto-select single event ONLY if:
160
+ // - No event is selected
161
+ // - Exactly one event available
162
+ // - Not loading
163
+ // - User didn't explicitly clear it (or it's initial load)
164
+ useEffect(() => {
165
+ const shouldAutoSelect =
166
+ !selectedEvent &&
167
+ events.length === 1 &&
168
+ !isLoading &&
169
+ (!userExplicitlyClearedRef.current || isInitialLoadRef.current);
170
+
171
+ if (shouldAutoSelect) {
172
+ console.log('[AppLayout] Auto-selecting single available event');
173
+ setSelectedEvent(events[0]);
174
+ } else if (!selectedEvent && events.length === 1 && userExplicitlyClearedRef.current) {
175
+ console.log('[AppLayout] Auto-select skipped - user explicitly cleared event');
176
+ }
177
+ }, [selectedEvent, events, isLoading, setSelectedEvent]);
178
+
179
+ // ... rest of your component
180
+ }
181
+ ```
182
+
183
+ ## Testing
184
+
185
+ After implementing the fix, test the following scenarios:
186
+
187
+ 1. **Initial Load**:
188
+ - ✅ Should auto-select if only one event is available
189
+
190
+ 2. **Switch Event to Organisation**:
191
+ - ✅ Should NOT auto-select event after switching to organisation
192
+ - ✅ Selector should show the organisation, not the event
193
+
194
+ 3. **Switch Organisation to Event**:
195
+ - ✅ Should select the event when user explicitly selects it
196
+ - ✅ Selector should show the event
197
+
198
+ 4. **Manual Event Selection**:
199
+ - ✅ Should select the event when user clicks on it
200
+ - ✅ Should not interfere with manual selections
201
+
202
+ ## Additional Notes
203
+
204
+ - pace-core's EventService already has internal logic to prevent auto-selection when `userClearedEventRef` is set
205
+ - This fix adds an additional layer of protection at the application level
206
+ - The ref-based approach ensures the flag persists across re-renders
207
+ - The initial load flag allows auto-selection on first page load while preventing it after user actions
208
+
209
+ ## Related pace-core Changes
210
+
211
+ The following changes were made in pace-core to support this fix:
212
+
213
+ 1. **ContextSelector** - Now prioritizes event selection over organisation when both are selected
214
+ 2. **Header Component** - Clears event selection when switching to organisation
215
+ 3. **EventService** - Preserves `userClearedEventRef` flag when organisation changes
216
+
217
+ These changes ensure that pace-core respects user intent, but consuming apps (like pace-mint) should also implement the above fix to prevent their own auto-selection logic from interfering.
218
+
@@ -0,0 +1,391 @@
1
+ ---
2
+ lastUpdated: 2025-12-31T00:00:00+11:00
3
+ version: 0.6.4
4
+ reviewedBy: pace-mint-implementation-guide
5
+ ---
6
+
7
+ # pace-mint RBAC Setup Guide
8
+
9
+ This guide explains how to configure pace-mint to support both organisation-based and event-based pages using pace-core's RBAC system.
10
+
11
+ ## Overview
12
+
13
+ pace-mint is a **hybrid app** that supports both:
14
+ - **Organisation-based pages**: Financial management at the organisation level
15
+ - **Event-based pages**: Budget and budget variables for specific events
16
+
17
+ Users can have:
18
+ - Organisation roles (org_admin, leader, member, etc.) for organisation-based pages
19
+ - Event-app roles (planner, participant, etc.) for event-based pages
20
+ - Both types of roles simultaneously
21
+
22
+ ## Architecture
23
+
24
+ ### Two-Stream Navigation
25
+
26
+ pace-mint uses a **two-stream architecture**:
27
+
28
+ 1. **Landing/Dashboard Page**: User selects their mode
29
+ - "Organisation Financial Management" → Organisation stream
30
+ - "Event Financial Management" → Event stream
31
+
32
+ 2. **Each Stream**: Has its own navigation showing only relevant pages
33
+ - Organisation stream: Shows only organisation-scoped pages
34
+ - Event stream: Shows only event-scoped pages
35
+
36
+ ### Page Scoping
37
+
38
+ Each page in pace-mint is marked with a `scope_type`:
39
+
40
+ - `'event'`: Page requires event context (e.g., "budget", "budget-variables")
41
+ - `'organisation'`: Page requires organisation context (e.g., "dashboard", "reports")
42
+ - `'both'`: Page can be accessed in either context (rare, but possible)
43
+
44
+ **Note**: All pages must have `scope_type` set explicitly. There is no inheritance from app-level configuration.
45
+
46
+ ## Database Setup
47
+
48
+ ### 1. Ensure pace-mint App Configuration
49
+
50
+ ```sql
51
+ -- Verify pace-mint app exists and is configured correctly
52
+ SELECT id, name, display_name, is_active
53
+ FROM rbac_apps
54
+ WHERE name = 'MINT';
55
+
56
+ -- Note: App-level scope configuration (requires_event) has been removed.
57
+ -- All scope is now configured at the page level via scope_type.
58
+ ```
59
+
60
+ ### 2. Create/Update Pages with scope_type
61
+
62
+ For each page in pace-mint, set the `scope_type`:
63
+
64
+ ```sql
65
+ -- Example: Budget page is event-based
66
+ UPDATE rbac_app_pages
67
+ SET scope_type = 'event'
68
+ WHERE app_id = (SELECT id FROM rbac_apps WHERE name = 'MINT')
69
+ AND page_name = 'budget';
70
+
71
+ -- Example: Dashboard page is organisation-based (or NULL to inherit)
72
+ UPDATE rbac_app_pages
73
+ SET scope_type = 'organisation'
74
+ WHERE app_id = (SELECT id FROM rbac_apps WHERE name = 'MINT')
75
+ AND page_name = 'dashboard';
76
+
77
+ -- Example: Budget variables page is event-based
78
+ UPDATE rbac_app_pages
79
+ SET scope_type = 'event'
80
+ WHERE app_id = (SELECT id FROM rbac_apps WHERE name = 'MINT')
81
+ AND page_name = 'budget-variables';
82
+ ```
83
+
84
+ **Important**:
85
+ - All pages must have `scope_type` set explicitly (no NULL values allowed)
86
+ - Set `scope_type` to match the page's context requirements
87
+ - There is no inheritance - each page declares its own scope
88
+
89
+ ### 3. Configure Page Permissions
90
+
91
+ Set up permissions for each page based on its scope:
92
+
93
+ #### Event-Based Pages (e.g., "budget")
94
+
95
+ ```sql
96
+ -- Get the page ID
97
+ WITH mint_app AS (
98
+ SELECT id FROM rbac_apps WHERE name = 'MINT'
99
+ ),
100
+ budget_page AS (
101
+ SELECT ap.id as page_id
102
+ FROM rbac_app_pages ap
103
+ JOIN mint_app ma ON ap.app_id = ma.id
104
+ WHERE ap.page_name = 'budget'
105
+ )
106
+ -- Insert permissions for event-based roles
107
+ INSERT INTO rbac_page_permissions (app_page_id, operation, role_name, allowed, organisation_id)
108
+ SELECT
109
+ bp.page_id,
110
+ op.operation,
111
+ role.role_name,
112
+ CASE
113
+ WHEN role.role_name = 'planner' THEN true
114
+ WHEN role.role_name = 'participant' AND op.operation = 'read' THEN true
115
+ ELSE false
116
+ END,
117
+ '00000000-0000-0000-0000-000000000000'::uuid -- ⚠️ REPLACE with actual organisation ID
118
+ FROM budget_page bp
119
+ CROSS JOIN (SELECT unnest(ARRAY['read', 'create', 'update', 'delete']) as operation) op
120
+ CROSS JOIN (SELECT unnest(ARRAY['planner', 'participant', 'viewer']) as role_name) role;
121
+ ```
122
+
123
+ #### Organisation-Based Pages (e.g., "dashboard")
124
+
125
+ ```sql
126
+ -- Get the page ID
127
+ WITH mint_app AS (
128
+ SELECT id FROM rbac_apps WHERE name = 'MINT'
129
+ ),
130
+ dashboard_page AS (
131
+ SELECT ap.id as page_id
132
+ FROM rbac_app_pages ap
133
+ JOIN mint_app ma ON ap.app_id = ma.id
134
+ WHERE ap.page_name = 'dashboard'
135
+ )
136
+ -- Insert permissions for organisation-based roles
137
+ INSERT INTO rbac_page_permissions (app_page_id, operation, role_name, allowed, organisation_id)
138
+ SELECT
139
+ dp.page_id,
140
+ op.operation,
141
+ role.role_name,
142
+ CASE
143
+ WHEN role.role_name = 'org_admin' THEN true
144
+ WHEN role.role_name = 'leader' AND op.operation != 'delete' THEN true
145
+ WHEN role.role_name = 'member' AND op.operation = 'read' THEN true
146
+ ELSE false
147
+ END,
148
+ '00000000-0000-0000-0000-000000000000'::uuid -- ⚠️ REPLACE with actual organisation ID
149
+ FROM dashboard_page dp
150
+ CROSS JOIN (SELECT unnest(ARRAY['read', 'create', 'update', 'delete']) as operation) op
151
+ CROSS JOIN (SELECT unnest(ARRAY['org_admin', 'leader', 'member', 'supporter']) as role_name) role;
152
+ ```
153
+
154
+ ## pace-core Components
155
+
156
+ ### 1. Context Selector Component
157
+
158
+ pace-core provides a unified `ContextSelector` component that intelligently shows all organisations and events a user can access:
159
+
160
+ ```tsx
161
+ import { ContextSelector } from '@jmruthers/pace-core';
162
+
163
+ <ContextSelector
164
+ onOrganisationSelect={(org) => {
165
+ // Switch to organisation stream
166
+ setSelectedOrganisation(org);
167
+ setSelectedEvent(null);
168
+ navigate('/dashboard'); // Navigate to org-based dashboard
169
+ }}
170
+ onEventSelect={(event) => {
171
+ // Switch to event stream
172
+ setSelectedEvent(event);
173
+ setSelectedOrganisation(null);
174
+ navigate('/budget'); // Navigate to event-based budget page
175
+ }}
176
+ />
177
+ ```
178
+
179
+ The `ContextSelector` automatically shows:
180
+ - All organisations the user has access to (via organisation roles)
181
+ - All events the user has access to (via event-app roles or organisation membership)
182
+ - Everything for super admins
183
+
184
+ ### 2. Navigation Filtering
185
+
186
+ pace-core's navigation automatically filters pages based on:
187
+ - Current context (event selected vs organisation selected)
188
+ - Page `scope_type`
189
+ - User's permissions
190
+
191
+ You don't need to manually filter - pace-core handles this automatically.
192
+
193
+ ### 3. Permission Checking
194
+
195
+ pace-core automatically handles permission checking based on page scope:
196
+
197
+ - **Event-based pages**: Checks event-app roles for the selected event
198
+ - **Organisation-based pages**: Checks organisation roles for the selected organisation
199
+ - **'both' pages**: Checks both scopes and returns the union (higher permissions win)
200
+
201
+ ## Implementation Steps
202
+
203
+ ### Step 1: Database Configuration
204
+
205
+ 1. Run the pace-core migration that adds `scope_type` to `rbac_app_pages`
206
+ 2. Update all pace-mint pages with appropriate `scope_type` values
207
+ 3. Configure page permissions for each scope type
208
+
209
+ ### Step 2: Landing Page
210
+
211
+ Create a landing page where users choose their mode:
212
+
213
+ ```tsx
214
+ function MintLandingPage() {
215
+ const { selectedOrganisation, selectedEvent } = useUnifiedAuth();
216
+ const navigate = useNavigate();
217
+
218
+ const handleOrgMode = () => {
219
+ // Ensure organisation is selected
220
+ if (!selectedOrganisation) {
221
+ // Show organisation selector
222
+ return;
223
+ }
224
+ navigate('/dashboard');
225
+ };
226
+
227
+ const handleEventMode = () => {
228
+ // Ensure event is selected
229
+ if (!selectedEvent) {
230
+ // Show event selector
231
+ return;
232
+ }
233
+ navigate('/budget');
234
+ };
235
+
236
+ return (
237
+ <main>
238
+ <section>
239
+ <h1>pace-mint</h1>
240
+ <button onClick={handleOrgMode}>
241
+ Organisation Financial Management
242
+ </button>
243
+ <button onClick={handleEventMode}>
244
+ Event Financial Management
245
+ </button>
246
+ </section>
247
+ </main>
248
+ );
249
+ }
250
+ ```
251
+
252
+ ### Step 3: Navigation Configuration
253
+
254
+ Configure navigation items with proper page names:
255
+
256
+ ```tsx
257
+ const orgNavItems = [
258
+ { label: 'Dashboard', pageName: 'dashboard', href: '/dashboard' },
259
+ { label: 'Reports', pageName: 'reports', href: '/reports' },
260
+ // ... other org-based pages
261
+ ];
262
+
263
+ const eventNavItems = [
264
+ { label: 'Budget', pageName: 'budget', href: '/budget' },
265
+ { label: 'Budget Variables', pageName: 'budget-variables', href: '/budget-variables' },
266
+ // ... other event-based pages
267
+ ];
268
+ ```
269
+
270
+ ### Step 4: Use Context Selector in Header
271
+
272
+ The `PaceAppLayout` component includes the unified `ContextSelector` by default:
273
+
274
+ ```tsx
275
+ import { PaceAppLayout } from '@jmruthers/pace-core';
276
+
277
+ function App() {
278
+ const { selectedOrganisation, selectedEvent, setSelectedOrganisation, setSelectedEvent } = useUnifiedAuth();
279
+ const navigate = useNavigate();
280
+
281
+ return (
282
+ <PaceAppLayout
283
+ appName="MINT"
284
+ navItems={/* your nav items */}
285
+ showContextSelector={true} // Default: shows unified selector
286
+ />
287
+ );
288
+ }
289
+ ```
290
+
291
+ The `ContextSelector` is automatically integrated into the header and handles organisation/event selection. You can also use it directly if you need custom behavior:
292
+
293
+ ```tsx
294
+ import { ContextSelector } from '@jmruthers/pace-core';
295
+
296
+ <ContextSelector
297
+ onOrganisationSelect={(org) => {
298
+ setSelectedOrganisation(org);
299
+ setSelectedEvent(null);
300
+ navigate('/dashboard');
301
+ }}
302
+ onEventSelect={(event) => {
303
+ setSelectedEvent(event);
304
+ setSelectedOrganisation(null);
305
+ navigate('/budget');
306
+ }}
307
+ />
308
+ ```
309
+
310
+ ### Step 5: Page Protection
311
+
312
+ Use `PagePermissionGuard` for all pages:
313
+
314
+ ```tsx
315
+ import { PagePermissionGuard } from '@jmruthers/pace-core/rbac';
316
+
317
+ function BudgetPage() {
318
+ return (
319
+ <PagePermissionGuard pageName="budget" operation="read">
320
+ {/* Budget page content */}
321
+ </PagePermissionGuard>
322
+ );
323
+ }
324
+ ```
325
+
326
+ ## Permission Precedence
327
+
328
+ When a user has both organisation and event permissions for a 'both' page:
329
+
330
+ 1. **Union of permissions**: User gets permissions from both scopes
331
+ 2. **Higher order wins**: More permissive permission takes precedence
332
+ - Access levels: `super` > `admin` > `user` > `none`
333
+ - Permission values: `true` > `false`
334
+
335
+ Example:
336
+ - User has `read:budget` at org level (org_admin)
337
+ - User has `write:budget` at event level (planner)
338
+ - Result: User gets `write:budget` (higher permission wins)
339
+
340
+ ## Super Admin Behavior
341
+
342
+ Super admins:
343
+ - See **all events** in the hybrid selector
344
+ - See **all organisations** in the hybrid selector
345
+ - Can access **all pages** regardless of scope
346
+ - Bypass all permission checks
347
+
348
+ ## Testing Checklist
349
+
350
+ - [ ] All pages have correct `scope_type` set in database
351
+ - [ ] Event-based pages only accessible when event is selected
352
+ - [ ] Organisation-based pages only accessible when organisation is selected
353
+ - [ ] Navigation filters correctly based on current context
354
+ - [ ] Context selector shows all accessible orgs and events
355
+ - [ ] Permission checks work correctly for each scope type
356
+ - [ ] 'both' pages return union of permissions
357
+ - [ ] Super admins can access all pages
358
+ - [ ] Users with both org and event roles see appropriate pages
359
+
360
+ ## Troubleshooting
361
+
362
+ ### Pages not showing in navigation
363
+
364
+ 1. Check page `scope_type` matches current context
365
+ 2. Verify user has permissions for the page
366
+ 3. Ensure page exists in `rbac_app_pages` table
367
+ 4. Check page permissions are configured in `rbac_page_permissions`
368
+
369
+ ### Permission denied errors
370
+
371
+ 1. Verify user has the required role for the page
372
+ 2. Check page `scope_type` matches selected context
373
+ 3. Ensure permissions are configured for the correct organisation/event
374
+ 4. Verify user's roles are active (valid_from/valid_to dates)
375
+
376
+ ### Context selector not showing items
377
+
378
+ 1. Check user has organisation memberships or event-app roles
379
+ 2. Verify organisations/events are active
380
+ 3. Check RLS policies allow user to see the items
381
+ 4. Ensure super admin status if testing as super admin
382
+
383
+ ## Related Documentation
384
+
385
+ - [RBAC Scope Migration Guide](../migration/RBAC_SCOPE_MIGRATION.md) - Complete migration guide
386
+ - [Context Selector Component](../components/context-selector.md) - Unified selector documentation
387
+ - [RBAC System Overview](../rbac/README.md)
388
+ - [Event-Based Apps](../rbac/event-based-apps.md)
389
+ - [Navigation Filtering](../components/NavigationMenu.md)
390
+ - [Page Permission Guard](../rbac/components/PagePermissionGuard.md)
391
+