@jmruthers/pace-core 0.2.5 → 0.2.7

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 (167) hide show
  1. package/dist/{DataTable-BHlzyKZP.d.ts → DataTable-C1AEm9Cx.d.ts} +1 -1
  2. package/dist/{DataTable-GEY5U7OI.js → DataTable-EEUDXPE5.js} +2 -8
  3. package/dist/{api-T6CBS7IO.js → api-ETQ6YJ3C.js} +2 -3
  4. package/dist/{chunk-DY5E3AT7.js → chunk-BEZRLNK3.js} +13 -3
  5. package/dist/chunk-BEZRLNK3.js.map +1 -0
  6. package/dist/{chunk-ANE4PDC2.js → chunk-C5G2A4PO.js} +159 -6
  7. package/dist/chunk-C5G2A4PO.js.map +1 -0
  8. package/dist/{chunk-WYB6MBZA.js → chunk-EWKPTNPO.js} +579 -973
  9. package/dist/chunk-EWKPTNPO.js.map +1 -0
  10. package/dist/{chunk-TMRLB2LA.js → chunk-HEMJ4SUJ.js} +2 -2
  11. package/dist/{chunk-O4T53L7X.js → chunk-HNDFPXUU.js} +5 -5
  12. package/dist/{chunk-UY7AM4QG.js → chunk-RRUYHORU.js} +161 -74
  13. package/dist/chunk-RRUYHORU.js.map +1 -0
  14. package/dist/{chunk-PFRRIDYA.js → chunk-TIVL4UQ7.js} +2 -2
  15. package/dist/{chunk-2MKP6IYD.js → chunk-VYG4AXYW.js} +2 -2
  16. package/dist/components.d.ts +2 -2
  17. package/dist/components.js +15 -16
  18. package/dist/components.js.map +1 -1
  19. package/dist/hooks.d.ts +1 -1
  20. package/dist/hooks.js +4 -4
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +16 -17
  23. package/dist/index.js.map +1 -1
  24. package/dist/providers.js +2 -2
  25. package/dist/rbac/index.js +25 -20
  26. package/dist/rbac/index.js.map +1 -1
  27. package/dist/styles/core.css +83 -62
  28. package/dist/{types-CInEi-ng.d.ts → types-DiRQsGJs.d.ts} +0 -2
  29. package/dist/utils.d.ts +2 -2
  30. package/dist/utils.js +1 -1
  31. package/docs/api/classes/ErrorBoundary.md +1 -1
  32. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  33. package/docs/api/interfaces/AggregateConfig.md +1 -1
  34. package/docs/api/interfaces/ButtonProps.md +1 -1
  35. package/docs/api/interfaces/CardProps.md +1 -1
  36. package/docs/api/interfaces/ColorPalette.md +1 -1
  37. package/docs/api/interfaces/ColorShade.md +1 -1
  38. package/docs/api/interfaces/DataTableAction.md +1 -1
  39. package/docs/api/interfaces/DataTableColumn.md +1 -1
  40. package/docs/api/interfaces/DataTableProps.md +33 -33
  41. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  42. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  43. package/docs/api/interfaces/EventContextType.md +1 -1
  44. package/docs/api/interfaces/EventLogoProps.md +1 -1
  45. package/docs/api/interfaces/EventProviderProps.md +1 -1
  46. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  47. package/docs/api/interfaces/FileUploadProps.md +1 -1
  48. package/docs/api/interfaces/FooterProps.md +1 -1
  49. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  50. package/docs/api/interfaces/InputProps.md +1 -1
  51. package/docs/api/interfaces/LabelProps.md +1 -1
  52. package/docs/api/interfaces/LoginFormProps.md +1 -1
  53. package/docs/api/interfaces/NavigationItem.md +1 -1
  54. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  55. package/docs/api/interfaces/Organisation.md +1 -1
  56. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  57. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  58. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  59. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  60. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  61. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  62. package/docs/api/interfaces/PaletteData.md +1 -1
  63. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  64. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  65. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  66. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  67. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  68. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  69. package/docs/api/interfaces/StorageConfig.md +1 -1
  70. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  71. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  72. package/docs/api/interfaces/StorageListOptions.md +1 -1
  73. package/docs/api/interfaces/StorageListResult.md +1 -1
  74. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  75. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  76. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  77. package/docs/api/interfaces/StyleImport.md +1 -1
  78. package/docs/api/interfaces/ToastActionElement.md +1 -1
  79. package/docs/api/interfaces/ToastProps.md +1 -1
  80. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  81. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  82. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  83. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  84. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  85. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  86. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  87. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  88. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  89. package/docs/api/interfaces/UserEventAccess.md +1 -1
  90. package/docs/api/interfaces/UserMenuProps.md +1 -1
  91. package/docs/api/interfaces/UserProfile.md +1 -1
  92. package/docs/api/modules.md +10 -10
  93. package/docs/architecture/README.md +1 -1
  94. package/package.json +1 -1
  95. package/src/__tests__/shared/testUtils.optimized.tsx +65 -7
  96. package/src/components/DataTable/DataTable.tsx +1 -3
  97. package/src/components/DataTable/__tests__/DataTable.errorHandling.test.tsx +0 -8
  98. package/src/components/DataTable/__tests__/DataTable.hierarchical.test.tsx +17 -12
  99. package/src/components/DataTable/__tests__/DataTable.infinite-loop.test.tsx +0 -1
  100. package/src/components/DataTable/__tests__/DataTable.integration.test.tsx +4 -12
  101. package/src/components/DataTable/__tests__/DataTable.performance.test.tsx +0 -8
  102. package/src/components/DataTable/__tests__/DataTable.permissions.test.tsx +21 -11
  103. package/src/components/DataTable/__tests__/DataTable.sorting.test.tsx +321 -0
  104. package/src/components/DataTable/__tests__/DataTable.userWorkflows.test.tsx +21 -11
  105. package/src/components/DataTable/__tests__/DataTable.workflowValidation.test.tsx +94 -0
  106. package/src/components/DataTable/__tests__/DataTable.workflows.test.tsx +25 -15
  107. package/src/components/DataTable/__tests__/README.md +11 -2
  108. package/src/components/DataTable/__tests__/performance-regression.test.tsx +0 -11
  109. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +0 -1
  110. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +2 -2
  111. package/src/components/DataTable/components/DataTableBody.tsx +34 -35
  112. package/src/components/DataTable/components/DataTableCore.tsx +205 -133
  113. package/src/components/DataTable/components/DataTableToolbar.tsx +9 -10
  114. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -7
  115. package/src/components/DataTable/components/EditableRow.tsx +6 -7
  116. package/src/components/DataTable/components/FilterRow.tsx +0 -1
  117. package/src/components/DataTable/components/GroupingDropdown.tsx +2 -2
  118. package/src/components/DataTable/components/UnifiedTableBody.tsx +83 -281
  119. package/src/components/DataTable/components/VirtualizedDataTable.tsx +9 -89
  120. package/src/components/DataTable/components/__tests__/DataTable.accessibility.test.tsx +111 -5
  121. package/src/components/DataTable/components/__tests__/DataTable.integration.test.tsx +82 -13
  122. package/src/components/DataTable/components/__tests__/DataTable.performance.test.tsx +0 -1
  123. package/src/components/DataTable/components/__tests__/DataTable.real.test.tsx +2 -2
  124. package/src/components/DataTable/components/__tests__/DataTable.security.test.tsx +0 -1
  125. package/src/components/DataTable/components/__tests__/DataTable.unit.test.tsx +2 -2
  126. package/src/components/DataTable/components/__tests__/FilteringToggle.unit.test.tsx +3 -0
  127. package/src/components/DataTable/components/index.ts +0 -1
  128. package/src/components/DataTable/core/DataTableContext.tsx +0 -1
  129. package/src/components/DataTable/index.ts +0 -2
  130. package/src/components/DataTable/types.ts +0 -2
  131. package/src/components/Input/Input.tsx +2 -2
  132. package/src/components/Input/__tests__/Input.unit.test.tsx +4 -4
  133. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +6 -2
  134. package/src/components/RBAC/PagePermissionGuard.tsx +13 -0
  135. package/src/components/RBAC/__tests__/PagePermissionGuard.unit.test.tsx +10 -1
  136. package/src/components/Select/Select.tsx +7 -1
  137. package/src/components/__tests__/EdgeCaseTesting.enhanced.test.tsx +2 -1
  138. package/src/hooks/__tests__/useRBAC.unit.test.ts +32 -24
  139. package/src/providers/RBACProvider.tsx +14 -2
  140. package/src/providers/__tests__/UnifiedAuthProvider.unit.test.tsx +11 -3
  141. package/src/rbac/__tests__/cache-invalidation.test.ts +2 -2
  142. package/src/rbac/__tests__/cache.test.ts +3 -3
  143. package/src/rbac/hooks.ts +15 -7
  144. package/src/styles/core.css +83 -62
  145. package/src/utils/__tests__/lazyLoad.unit.test.tsx +13 -18
  146. package/src/utils/storage/__tests__/helpers.unit.test.ts +9 -7
  147. package/dist/cache-I72HKDOA.js +0 -12
  148. package/dist/cache-I72HKDOA.js.map +0 -1
  149. package/dist/chunk-ANE4PDC2.js.map +0 -1
  150. package/dist/chunk-DY5E3AT7.js.map +0 -1
  151. package/dist/chunk-MRRFJ6SA.js +0 -161
  152. package/dist/chunk-MRRFJ6SA.js.map +0 -1
  153. package/dist/chunk-UY7AM4QG.js.map +0 -1
  154. package/dist/chunk-WYB6MBZA.js.map +0 -1
  155. package/src/components/DataTable/__tests__/DataTable.autoSizing.test.tsx +0 -526
  156. package/src/components/DataTable/components/DataTableHeader.tsx +0 -31
  157. package/src/components/DataTable/components/__tests__/DataTableHeader.unit.test.tsx +0 -143
  158. package/src/components/DataTable/examples/AutoSizingExample.tsx +0 -180
  159. package/src/components/DataTable/examples/ColumnSizingComparison.tsx +0 -235
  160. package/src/components/DataTable/utils/__tests__/columnSizing.test.ts +0 -237
  161. package/src/components/DataTable/utils/columnSizing.ts +0 -125
  162. /package/dist/{DataTable-GEY5U7OI.js.map → DataTable-EEUDXPE5.js.map} +0 -0
  163. /package/dist/{api-T6CBS7IO.js.map → api-ETQ6YJ3C.js.map} +0 -0
  164. /package/dist/{chunk-TMRLB2LA.js.map → chunk-HEMJ4SUJ.js.map} +0 -0
  165. /package/dist/{chunk-O4T53L7X.js.map → chunk-HNDFPXUU.js.map} +0 -0
  166. /package/dist/{chunk-PFRRIDYA.js.map → chunk-TIVL4UQ7.js.map} +0 -0
  167. /package/dist/{chunk-2MKP6IYD.js.map → chunk-VYG4AXYW.js.map} +0 -0
@@ -142,8 +142,12 @@ export function PagePermissionGuard({
142
142
 
143
143
  // Load permissions for the page
144
144
  useEffect(() => {
145
+ let isMounted = true;
146
+
145
147
  const loadPermissions = async () => {
146
148
  try {
149
+ if (!isMounted) return;
150
+
147
151
  setPermissions(prev => ({ ...prev, isLoading: true, error: null }));
148
152
 
149
153
  const permissionChecks: Array<[Operation, string]> = [
@@ -155,6 +159,8 @@ export function PagePermissionGuard({
155
159
 
156
160
  const results = await checkMultiplePermissions(permissionChecks, cacheTTL);
157
161
 
162
+ if (!isMounted) return;
163
+
158
164
  const newPermissions: PagePermissions = {
159
165
  canRead: results.find(r => r.operation === 'read')?.hasPermission || false,
160
166
  canCreate: results.find(r => r.operation === 'create')?.hasPermission || false,
@@ -176,6 +182,8 @@ export function PagePermissionGuard({
176
182
 
177
183
  setPermissions(newPermissions);
178
184
  } catch (error) {
185
+ if (!isMounted) return;
186
+
179
187
  console.error(`[PagePermissionGuard] Error loading permissions for ${pageId}:`, error);
180
188
  setPermissions({
181
189
  canRead: false,
@@ -189,6 +197,11 @@ export function PagePermissionGuard({
189
197
  };
190
198
 
191
199
  loadPermissions();
200
+
201
+ // Cleanup function
202
+ return () => {
203
+ isMounted = false;
204
+ };
192
205
  }, [pageId, checkMultiplePermissions, getDebugInfo, enableDebug, cacheTTL]);
193
206
 
194
207
  // Show loading state
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { screen, waitFor } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
- import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
5
  import {
6
6
  PagePermissionGuard,
7
7
  ReadPermissionGuard,
@@ -48,6 +48,15 @@ const TestComponent = ({
48
48
  );
49
49
 
50
50
  describe('PagePermissionGuard', () => {
51
+ afterEach(() => {
52
+ // Clean up any pending async operations
53
+ if (vi.isFakeTimers()) {
54
+ vi.runOnlyPendingTimers();
55
+ vi.useRealTimers();
56
+ }
57
+ // Clear all mocks
58
+ vi.clearAllMocks();
59
+ });
51
60
  beforeEach(() => {
52
61
  vi.clearAllMocks();
53
62
  });
@@ -179,16 +179,22 @@ export const Select = React.forwardRef<HTMLFormElement, SelectProps>(
179
179
  // Listen for SelectItem mousedown events to set selecting flag
180
180
  React.useEffect(() => {
181
181
  let timeoutId: NodeJS.Timeout | null = null;
182
+ let isMounted = true;
182
183
 
183
184
  const handleSelectItemMouseDown = () => {
185
+ if (!isMounted) return;
186
+
184
187
  setIsSelecting(true);
185
188
  timeoutId = setTimeout(() => {
186
- setIsSelecting(false);
189
+ if (isMounted) {
190
+ setIsSelecting(false);
191
+ }
187
192
  }, 150);
188
193
  };
189
194
 
190
195
  document.addEventListener('selectItemMouseDown', handleSelectItemMouseDown as EventListener);
191
196
  return () => {
197
+ isMounted = false;
192
198
  document.removeEventListener('selectItemMouseDown', handleSelectItemMouseDown as EventListener);
193
199
  if (timeoutId) {
194
200
  clearTimeout(timeoutId);
@@ -98,7 +98,8 @@ export const edgeCaseUtils = {
98
98
 
99
99
  // Component should still be functional
100
100
  const article = screen.queryByRole('article');
101
- const button = screen.queryByRole('button');
101
+ const buttons = screen.queryAllByRole('button');
102
+ const button = buttons.length > 0 ? buttons[0] : null;
102
103
  const table = screen.queryByRole('table');
103
104
  const textbox = screen.queryByRole('textbox');
104
105
  const form = screen.queryByRole('form', { hidden: true });
@@ -4,20 +4,16 @@ import { useRBAC } from '../useRBAC';
4
4
  import { createMockSupabaseClient, testDataGenerators } from '../../__tests__/shared';
5
5
 
6
6
  // Mock the providers
7
- const mockUseUnifiedAuth = vi.fn();
8
- const mockUseOrganisations = vi.fn();
9
- const mockUseEvents = vi.fn();
10
-
11
7
  vi.mock('../../providers/UnifiedAuthProvider', () => ({
12
- useUnifiedAuth: () => mockUseUnifiedAuth()
8
+ useUnifiedAuth: vi.fn()
13
9
  }));
14
10
 
15
11
  vi.mock('../../providers/OrganisationProvider', () => ({
16
- useOrganisations: () => mockUseOrganisations()
12
+ useOrganisations: vi.fn()
17
13
  }));
18
14
 
19
15
  vi.mock('../../providers/EventProvider', () => ({
20
- useEvents: () => mockUseEvents()
16
+ useEvents: vi.fn()
21
17
  }));
22
18
 
23
19
  // Mock Supabase client
@@ -52,7 +48,7 @@ if (mockSupabase.rpc) {
52
48
  (mockSupabase as any).from = mockFrom;
53
49
 
54
50
  describe('useRBAC', () => {
55
- beforeEach(() => {
51
+ beforeEach(async () => {
56
52
  vi.clearAllMocks();
57
53
 
58
54
  // Reset the from mock
@@ -80,26 +76,31 @@ describe('useRBAC', () => {
80
76
  });
81
77
  }
82
78
 
83
- // Default mock implementations
84
- mockUseUnifiedAuth.mockReturnValue({
79
+ // Mock the actual hook calls
80
+ const { useUnifiedAuth } = await import('../../providers/UnifiedAuthProvider');
81
+ const { useOrganisations } = await import('../../providers/OrganisationProvider');
82
+ const { useEvents } = await import('../../providers/EventProvider');
83
+
84
+ vi.mocked(useUnifiedAuth).mockReturnValue({
85
85
  user: { id: 'test-user-id' },
86
86
  session: { access_token: 'test-token' },
87
87
  supabase: mockSupabase,
88
88
  appName: 'test-app'
89
89
  });
90
90
 
91
- mockUseOrganisations.mockReturnValue({
91
+ vi.mocked(useOrganisations).mockReturnValue({
92
92
  selectedOrganisation: { id: 'test-org-id' }
93
93
  });
94
94
 
95
- mockUseEvents.mockReturnValue({
95
+ vi.mocked(useEvents).mockReturnValue({
96
96
  selectedEvent: { event_id: 'test-event-id' }
97
97
  });
98
98
  });
99
99
 
100
100
  describe('Initial State', () => {
101
- it('returns initial state when no user is provided', () => {
102
- mockUseUnifiedAuth.mockReturnValue({
101
+ it('returns initial state when no user is provided', async () => {
102
+ const { useUnifiedAuth } = await import('../../providers/UnifiedAuthProvider');
103
+ vi.mocked(useUnifiedAuth).mockReturnValue({
103
104
  user: null,
104
105
  supabase: null,
105
106
  appName: null
@@ -114,8 +115,9 @@ describe('useRBAC', () => {
114
115
  expect(result.current.error).toBeNull();
115
116
  });
116
117
 
117
- it('returns initial state when no supabase client is provided', () => {
118
- mockUseUnifiedAuth.mockReturnValue({
118
+ it('returns initial state when no supabase client is provided', async () => {
119
+ const { useUnifiedAuth } = await import('../../providers/UnifiedAuthProvider');
120
+ vi.mocked(useUnifiedAuth).mockReturnValue({
119
121
  user: { id: 'test-user-id' },
120
122
  supabase: null,
121
123
  appName: 'test-app'
@@ -130,8 +132,9 @@ describe('useRBAC', () => {
130
132
  expect(result.current.error).toBeNull();
131
133
  });
132
134
 
133
- it('returns initial state when no app name is provided', () => {
134
- mockUseUnifiedAuth.mockReturnValue({
135
+ it('returns initial state when no app name is provided', async () => {
136
+ const { useUnifiedAuth } = await import('../../providers/UnifiedAuthProvider');
137
+ vi.mocked(useUnifiedAuth).mockReturnValue({
135
138
  user: { id: 'test-user-id' },
136
139
  supabase: mockSupabase,
137
140
  appName: null
@@ -758,7 +761,8 @@ describe('useRBAC', () => {
758
761
  });
759
762
 
760
763
  // Change user
761
- mockUseUnifiedAuth.mockReturnValue({
764
+ const { useUnifiedAuth } = await import('../../providers/UnifiedAuthProvider');
765
+ vi.mocked(useUnifiedAuth).mockReturnValue({
762
766
  user: { id: 'different-user-id' },
763
767
  session: { access_token: 'test-token' },
764
768
  supabase: mockSupabase,
@@ -794,7 +798,8 @@ describe('useRBAC', () => {
794
798
  }, { timeout: 3000 });
795
799
 
796
800
  // Change organisation
797
- mockUseOrganisations.mockReturnValue({
801
+ const { useOrganisations } = await import('../../providers/OrganisationProvider');
802
+ vi.mocked(useOrganisations).mockReturnValue({
798
803
  selectedOrganisation: { id: 'different-org-id' }
799
804
  });
800
805
 
@@ -827,7 +832,8 @@ describe('useRBAC', () => {
827
832
  });
828
833
 
829
834
  // Change event
830
- mockUseEvents.mockReturnValue({
835
+ const { useEvents } = await import('../../providers/EventProvider');
836
+ vi.mocked(useEvents).mockReturnValue({
831
837
  selectedEvent: { event_id: 'different-event-id' }
832
838
  });
833
839
 
@@ -838,11 +844,12 @@ describe('useRBAC', () => {
838
844
  });
839
845
  });
840
846
 
841
- it('handles missing EventProvider gracefully', () => {
847
+ it('handles missing EventProvider gracefully', async () => {
842
848
  const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
843
849
 
844
850
  // Mock EventProvider to throw error
845
- mockUseEvents.mockImplementation(() => {
851
+ const { useEvents } = await import('../../providers/EventProvider');
852
+ vi.mocked(useEvents).mockImplementation(() => {
846
853
  throw new Error('EventProvider not available');
847
854
  });
848
855
 
@@ -873,7 +880,8 @@ describe('useRBAC', () => {
873
880
  it('includes user in returned context', async () => {
874
881
  const mockUser = testDataGenerators.createUser();
875
882
 
876
- mockUseUnifiedAuth.mockReturnValue({
883
+ const { useUnifiedAuth } = await import('../../providers/UnifiedAuthProvider');
884
+ vi.mocked(useUnifiedAuth).mockReturnValue({
877
885
  user: mockUser,
878
886
  supabase: mockSupabase,
879
887
  appName: 'test-app'
@@ -530,13 +530,25 @@ export function RBACProvider({
530
530
 
531
531
  // Load user event access when user changes
532
532
  useEffect(() => {
533
+ let isMounted = true;
534
+
533
535
  if (user && session) {
534
536
  DebugLogger.log('RBACProvider', 'Loading user event access for authenticated user');
535
- loadUserEventAccess();
537
+ loadUserEventAccess().catch(error => {
538
+ if (isMounted) {
539
+ console.error('Error loading user event access:', error);
540
+ }
541
+ });
536
542
  } else {
537
543
  DebugLogger.log('RBACProvider', 'Clearing user event access - no user or session');
538
- setUserEventAccess([]);
544
+ if (isMounted) {
545
+ setUserEventAccess([]);
546
+ }
539
547
  }
548
+
549
+ return () => {
550
+ isMounted = false;
551
+ };
540
552
  }, [user, session, loadUserEventAccess]);
541
553
 
542
554
  // Permission methods
@@ -11,7 +11,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
11
11
  import '@testing-library/jest-dom';
12
12
  import { createClient } from '@supabase/supabase-js';
13
13
  import { UnifiedAuthProvider, useUnifiedAuth } from '../UnifiedAuthProvider';
14
- import { renderWithProviders, createMockSupabaseClient } from '../../__tests__/shared';
14
+ import { renderWithProviders, createMockSupabaseClient, resetSharedSupabaseMock } from '../../__tests__/shared';
15
15
  import { AccessLevel } from '../../types/unified';
16
16
 
17
17
  // Mock Supabase client
@@ -25,10 +25,16 @@ describe('UnifiedAuthProvider', () => {
25
25
  beforeEach(() => {
26
26
  mockSupabaseClient = createMockSupabaseClient();
27
27
  (createClient as any).mockReturnValue(mockSupabaseClient);
28
+ resetSharedSupabaseMock();
28
29
  });
29
30
 
30
31
  afterEach(() => {
31
32
  vi.clearAllMocks();
33
+ // Clean up any pending async operations
34
+ if (vi.isFakeTimers()) {
35
+ vi.runOnlyPendingTimers();
36
+ vi.useRealTimers();
37
+ }
32
38
  });
33
39
 
34
40
  const TestComponent = () => {
@@ -68,7 +74,8 @@ describe('UnifiedAuthProvider', () => {
68
74
 
69
75
  it('should handle authentication state changes', async () => {
70
76
  let authCallback: any;
71
- mockSupabaseClient.auth.onAuthStateChange.mockImplementation((callback: any) => {
77
+ // Override the mock implementation for this specific test
78
+ vi.spyOn(mockSupabaseClient.auth, 'onAuthStateChange').mockImplementation((callback: any) => {
72
79
  authCallback = callback;
73
80
  return { data: { subscription: { unsubscribe: vi.fn() } } };
74
81
  });
@@ -118,7 +125,8 @@ describe('UnifiedAuthProvider', () => {
118
125
  );
119
126
  };
120
127
 
121
- mockSupabaseClient.auth.signInWithPassword.mockResolvedValue({
128
+ // The mock is already set up in the shared mock, but we can override it for this test
129
+ vi.spyOn(mockSupabaseClient.auth, 'signInWithPassword').mockResolvedValue({
122
130
  data: { user: { id: 'test-user' }, session: {} },
123
131
  error: null
124
132
  });
@@ -135,7 +135,7 @@ describe('Cache & Invalidation', () => {
135
135
  };
136
136
 
137
137
  const generatedKey = RBACCache.generatePermissionKey(key);
138
- const expectedKey = 'perm:user-123:org-456:event-789:app-101';
138
+ const expectedKey = 'perm:user-123:org-456:event-789:app-101:read:events:page-999';
139
139
 
140
140
  expect(generatedKey).toBe(expectedKey);
141
141
  });
@@ -172,7 +172,7 @@ describe('Cache & Invalidation', () => {
172
172
  };
173
173
 
174
174
  const generatedKey = RBACCache.generatePermissionKey(key);
175
- const expectedKey = 'perm:user-123:org-456:null:null';
175
+ const expectedKey = 'perm:user-123:org-456:null:null:null:null';
176
176
 
177
177
  expect(generatedKey).toBe(expectedKey);
178
178
  });
@@ -230,7 +230,7 @@ describe('RBACCache', () => {
230
230
  organisationId: 'org-456',
231
231
  });
232
232
 
233
- expect(key1).toBe('perm:user-123:org-456:null:null');
233
+ expect(key1).toBe('perm:user-123:org-456:null:null:null:null');
234
234
 
235
235
  const key2 = RBACCache.generatePermissionKey({
236
236
  userId: 'user-123',
@@ -239,7 +239,7 @@ describe('RBACCache', () => {
239
239
  appId: 'app-101',
240
240
  });
241
241
 
242
- expect(key2).toBe('perm:user-123:org-456:event-789:app-101');
242
+ expect(key2).toBe('perm:user-123:org-456:event-789:app-101:null:null');
243
243
  });
244
244
 
245
245
  it('should handle missing optional fields', () => {
@@ -247,7 +247,7 @@ describe('RBACCache', () => {
247
247
  userId: 'user-123',
248
248
  });
249
249
 
250
- expect(key).toBe('perm:user-123:null:null:null');
250
+ expect(key).toBe('perm:user-123:null:null:null:null:null');
251
251
  });
252
252
  });
253
253
 
package/src/rbac/hooks.ts CHANGED
@@ -133,13 +133,6 @@ export function useCan(
133
133
  console.log('[useCan] check() called with:', { userId, scope, permission, pageId });
134
134
  console.log('[useCan] Hook parameters:', { userId, scope, permission, pageId, useCache });
135
135
 
136
- // Clear cache for debugging - remove this after fixing
137
- if (typeof window !== 'undefined') {
138
- console.log('[useCan] Clearing cache for debugging...');
139
- const { rbacCache } = await import('./cache');
140
- rbacCache.clear();
141
- }
142
-
143
136
  if (!userId) {
144
137
  console.log('[useCan] No userId, denying access');
145
138
  setCan(false);
@@ -147,6 +140,21 @@ export function useCan(
147
140
  return;
148
141
  }
149
142
 
143
+ // Check for super admin status first - super admins bypass all scope requirements
144
+ try {
145
+ const { isSuperAdmin } = await import('./api');
146
+ const isSuper = await isSuperAdmin(userId);
147
+ if (isSuper) {
148
+ console.log('[useCan] User is super admin, granting access');
149
+ setCan(true);
150
+ setIsLoading(false);
151
+ return;
152
+ }
153
+ } catch (error) {
154
+ console.error('[useCan] Error checking super admin status:', error);
155
+ // Continue with normal permission check if super admin check fails
156
+ }
157
+
150
158
  // Check if scope is incomplete (missing required fields)
151
159
  if (!scope || !scope.organisationId || !scope.appId) {
152
160
  console.log('[useCan] Incomplete scope, waiting for resolution:', scope);
@@ -21,7 +21,7 @@
21
21
  @theme static {
22
22
 
23
23
  --app-width: 90rem;
24
-
24
+
25
25
  /* MAIN palette - silver */
26
26
  --color-main-raw: oklch(0.7 0.057 252.02);
27
27
  --color-main-50: oklch(0.98 0.001 252.02);
@@ -35,7 +35,7 @@
35
35
  --color-main-800: oklch(0.456 0.034 252.02);
36
36
  --color-main-900: oklch(0.332 0.024 252.02);
37
37
  --color-main-950: oklch(0.195 0.014 252.02);
38
-
38
+
39
39
  /* SEC palette - violet */
40
40
  --color-sec-raw: oklch(0.58 0.23 280.75);
41
41
  --color-sec-50: oklch(0.98 0.003 280.75);
@@ -49,7 +49,7 @@
49
49
  --color-sec-800: oklch(0.456 0.158 280.75);
50
50
  --color-sec-900: oklch(0.332 0.099 280.75);
51
51
  --color-sec-950: oklch(0.195 0.047 280.75);
52
-
52
+
53
53
  /* ACC palette - blood orange */
54
54
  --color-acc-raw: oklch(0.64 0.21 37.76);
55
55
  --color-acc-50: oklch(0.98 0.003 37.76);
@@ -97,10 +97,14 @@
97
97
  --print-first-page-margin: 1in;
98
98
  --print-cover-logo-size: 120px;
99
99
  --print-cover-title-size: 2.5rem;
100
- --print-page-width: 8.27in; /* A4 width */
101
- --print-page-height: 11.69in; /* A4 height */
102
- --print-landscape-width: 11.69in; /* A4 landscape width */
103
- --print-landscape-height: 8.27in; /* A4 landscape height */
100
+ --print-page-width: 8.27in;
101
+ /* A4 width */
102
+ --print-page-height: 11.69in;
103
+ /* A4 height */
104
+ --print-landscape-width: 11.69in;
105
+ /* A4 landscape width */
106
+ --print-landscape-height: 8.27in;
107
+ /* A4 landscape height */
104
108
  }
105
109
 
106
110
  @layer base {
@@ -117,47 +121,51 @@
117
121
  }
118
122
 
119
123
  /* Font definitions - Loading from consuming app's public directory with system font fallbacks */
120
- /* Font definitions */
121
- @font-face {
122
- font-family: "Georama";
123
- font-style: normal;
124
- font-weight: 100 200 300 400 500 600 700 800 900;
125
- font-display: swap;
126
- src: url("/fonts/georama.woff2") format("woff2");
127
- }
124
+ /* Font definitions */
125
+ @font-face {
126
+ font-family: "Georama";
127
+ font-style: normal;
128
+ font-weight: 100 200 300 400 500 600 700 800 900;
129
+ font-display: swap;
130
+ src: url("/fonts/georama.woff2") format("woff2");
131
+ }
128
132
 
129
- @font-face {
130
- font-family: "Georama";
131
- font-style: italic;
132
- font-weight: 100 200 300 400 500 600 700 800 900;
133
- font-display: swap;
134
- src: url("/fonts/georama-italic.woff2") format("woff2");
135
- }
133
+ @font-face {
134
+ font-family: "Georama";
135
+ font-style: italic;
136
+ font-weight: 100 200 300 400 500 600 700 800 900;
137
+ font-display: swap;
138
+ src: url("/fonts/georama-italic.woff2") format("woff2");
139
+ }
136
140
 
137
- @font-face {
138
- font-family: "Open Sans";
139
- font-style: normal;
140
- font-weight: 100 200 300 400 500 600 700 800 900;
141
- font-display: swap;
142
- src: url("/fonts/open-sans.woff2") format("woff2");
143
- }
141
+ @font-face {
142
+ font-family: "Open Sans";
143
+ font-style: normal;
144
+ font-weight: 100 200 300 400 500 600 700 800 900;
145
+ font-display: swap;
146
+ src: url("/fonts/open-sans.woff2") format("woff2");
147
+ }
144
148
 
145
- @font-face {
146
- font-family: "Open Sans";
147
- font-style: italic;
148
- font-weight: 100 200 300 400 500 600 700 800 900;
149
- font-display: swap;
150
- src: url("/fonts/open-sans-italic.woff2") format("woff2");
151
- }
149
+ @font-face {
150
+ font-family: "Open Sans";
151
+ font-style: italic;
152
+ font-weight: 100 200 300 400 500 600 700 800 900;
153
+ font-display: swap;
154
+ src: url("/fonts/open-sans-italic.woff2") format("woff2");
155
+ }
156
+
157
+ @font-face {
158
+ font-family: "Reddit Mono";
159
+ font-style: normal;
160
+ font-weight: 200 300 400 500 600 700 800 900;
161
+ font-display: swap;
162
+ src: url("/fonts/reddit-mono.woff2") format("woff2");
163
+ }
152
164
 
153
- @font-face {
154
- font-family: "Reddit Mono";
155
- font-style: normal;
156
- font-weight: 200 300 400 500 600 700 800 900;
157
- font-display: swap;
158
- src: url("/fonts/reddit-mono.woff2") format("woff2");
159
165
  }
160
166
 
167
+ @layer components {
168
+
161
169
  /* Elements */
162
170
  h1,
163
171
  h2,
@@ -301,16 +309,21 @@
301
309
  }
302
310
  }
303
311
 
304
- @layer components {
305
- /* Custom component styles go here */
306
- }
307
-
308
312
  @layer utilities {
313
+
309
314
  /* Print-specific utilities */
310
- .print-break-avoid { page-break-inside: avoid; }
311
- .print-break-before { page-break-before: always; }
312
- .print-break-after { page-break-after: always; }
313
-
315
+ .print-break-avoid {
316
+ page-break-inside: avoid;
317
+ }
318
+
319
+ .print-break-before {
320
+ page-break-before: always;
321
+ }
322
+
323
+ .print-break-after {
324
+ page-break-after: always;
325
+ }
326
+
314
327
  /* Print page size controls */
315
328
  .print-a4-portrait {
316
329
  @media print {
@@ -320,7 +333,7 @@
320
333
  }
321
334
  }
322
335
  }
323
-
336
+
324
337
  .print-a4-landscape {
325
338
  @media print {
326
339
  @page {
@@ -329,7 +342,7 @@
329
342
  }
330
343
  }
331
344
  }
332
-
345
+
333
346
  /* First page header styles */
334
347
  .print-first-page-header {
335
348
  @media print {
@@ -338,7 +351,7 @@
338
351
  }
339
352
  }
340
353
  }
341
-
354
+
342
355
  .print-cover-header {
343
356
  @media print {
344
357
  @page :first {
@@ -346,7 +359,7 @@
346
359
  }
347
360
  }
348
361
  }
349
-
362
+
350
363
  .print-subsequent-header {
351
364
  @media print {
352
365
  @page :not(:first) {
@@ -354,30 +367,38 @@
354
367
  }
355
368
  }
356
369
  }
357
-
370
+
358
371
  /* Print-specific layouts */
359
372
  @media print {
360
- .print-hidden { display: none !important; }
361
- .print-visible { display: block !important; }
362
- .print-first-page-only {
373
+ .print-hidden {
374
+ display: none !important;
375
+ }
376
+
377
+ .print-visible {
378
+ display: block !important;
379
+ }
380
+
381
+ .print-first-page-only {
363
382
  display: block;
364
383
  }
384
+
365
385
  .print-subsequent-pages-only {
366
386
  display: none;
367
387
  }
368
388
  }
369
-
389
+
370
390
  @media print {
371
391
  @page :not(:first) {
372
- .print-first-page-only {
392
+ .print-first-page-only {
373
393
  display: none !important;
374
394
  }
395
+
375
396
  .print-subsequent-pages-only {
376
397
  display: block !important;
377
398
  }
378
399
  }
379
400
  }
380
-
401
+
381
402
  /* Print typography */
382
403
  .print-text {
383
404
  @media print {
@@ -386,7 +407,7 @@
386
407
  color: var(--print-color);
387
408
  }
388
409
  }
389
-
410
+
390
411
  /* Print layout container */
391
412
  .print-layout {
392
413
  @media print {