@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.
- package/dist/{DataTable-BHlzyKZP.d.ts → DataTable-C1AEm9Cx.d.ts} +1 -1
- package/dist/{DataTable-GEY5U7OI.js → DataTable-EEUDXPE5.js} +2 -8
- package/dist/{api-T6CBS7IO.js → api-ETQ6YJ3C.js} +2 -3
- package/dist/{chunk-DY5E3AT7.js → chunk-BEZRLNK3.js} +13 -3
- package/dist/chunk-BEZRLNK3.js.map +1 -0
- package/dist/{chunk-ANE4PDC2.js → chunk-C5G2A4PO.js} +159 -6
- package/dist/chunk-C5G2A4PO.js.map +1 -0
- package/dist/{chunk-WYB6MBZA.js → chunk-EWKPTNPO.js} +579 -973
- package/dist/chunk-EWKPTNPO.js.map +1 -0
- package/dist/{chunk-TMRLB2LA.js → chunk-HEMJ4SUJ.js} +2 -2
- package/dist/{chunk-O4T53L7X.js → chunk-HNDFPXUU.js} +5 -5
- package/dist/{chunk-UY7AM4QG.js → chunk-RRUYHORU.js} +161 -74
- package/dist/chunk-RRUYHORU.js.map +1 -0
- package/dist/{chunk-PFRRIDYA.js → chunk-TIVL4UQ7.js} +2 -2
- package/dist/{chunk-2MKP6IYD.js → chunk-VYG4AXYW.js} +2 -2
- package/dist/components.d.ts +2 -2
- package/dist/components.js +15 -16
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -17
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +25 -20
- package/dist/rbac/index.js.map +1 -1
- package/dist/styles/core.css +83 -62
- package/dist/{types-CInEi-ng.d.ts → types-DiRQsGJs.d.ts} +0 -2
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +33 -33
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +10 -10
- package/docs/architecture/README.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/shared/testUtils.optimized.tsx +65 -7
- package/src/components/DataTable/DataTable.tsx +1 -3
- package/src/components/DataTable/__tests__/DataTable.errorHandling.test.tsx +0 -8
- package/src/components/DataTable/__tests__/DataTable.hierarchical.test.tsx +17 -12
- package/src/components/DataTable/__tests__/DataTable.infinite-loop.test.tsx +0 -1
- package/src/components/DataTable/__tests__/DataTable.integration.test.tsx +4 -12
- package/src/components/DataTable/__tests__/DataTable.performance.test.tsx +0 -8
- package/src/components/DataTable/__tests__/DataTable.permissions.test.tsx +21 -11
- package/src/components/DataTable/__tests__/DataTable.sorting.test.tsx +321 -0
- package/src/components/DataTable/__tests__/DataTable.userWorkflows.test.tsx +21 -11
- package/src/components/DataTable/__tests__/DataTable.workflowValidation.test.tsx +94 -0
- package/src/components/DataTable/__tests__/DataTable.workflows.test.tsx +25 -15
- package/src/components/DataTable/__tests__/README.md +11 -2
- package/src/components/DataTable/__tests__/performance-regression.test.tsx +0 -11
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +0 -1
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +2 -2
- package/src/components/DataTable/components/DataTableBody.tsx +34 -35
- package/src/components/DataTable/components/DataTableCore.tsx +205 -133
- package/src/components/DataTable/components/DataTableToolbar.tsx +9 -10
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -7
- package/src/components/DataTable/components/EditableRow.tsx +6 -7
- package/src/components/DataTable/components/FilterRow.tsx +0 -1
- package/src/components/DataTable/components/GroupingDropdown.tsx +2 -2
- package/src/components/DataTable/components/UnifiedTableBody.tsx +83 -281
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +9 -89
- package/src/components/DataTable/components/__tests__/DataTable.accessibility.test.tsx +111 -5
- package/src/components/DataTable/components/__tests__/DataTable.integration.test.tsx +82 -13
- package/src/components/DataTable/components/__tests__/DataTable.performance.test.tsx +0 -1
- package/src/components/DataTable/components/__tests__/DataTable.real.test.tsx +2 -2
- package/src/components/DataTable/components/__tests__/DataTable.security.test.tsx +0 -1
- package/src/components/DataTable/components/__tests__/DataTable.unit.test.tsx +2 -2
- package/src/components/DataTable/components/__tests__/FilteringToggle.unit.test.tsx +3 -0
- package/src/components/DataTable/components/index.ts +0 -1
- package/src/components/DataTable/core/DataTableContext.tsx +0 -1
- package/src/components/DataTable/index.ts +0 -2
- package/src/components/DataTable/types.ts +0 -2
- package/src/components/Input/Input.tsx +2 -2
- package/src/components/Input/__tests__/Input.unit.test.tsx +4 -4
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +6 -2
- package/src/components/RBAC/PagePermissionGuard.tsx +13 -0
- package/src/components/RBAC/__tests__/PagePermissionGuard.unit.test.tsx +10 -1
- package/src/components/Select/Select.tsx +7 -1
- package/src/components/__tests__/EdgeCaseTesting.enhanced.test.tsx +2 -1
- package/src/hooks/__tests__/useRBAC.unit.test.ts +32 -24
- package/src/providers/RBACProvider.tsx +14 -2
- package/src/providers/__tests__/UnifiedAuthProvider.unit.test.tsx +11 -3
- package/src/rbac/__tests__/cache-invalidation.test.ts +2 -2
- package/src/rbac/__tests__/cache.test.ts +3 -3
- package/src/rbac/hooks.ts +15 -7
- package/src/styles/core.css +83 -62
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +13 -18
- package/src/utils/storage/__tests__/helpers.unit.test.ts +9 -7
- package/dist/cache-I72HKDOA.js +0 -12
- package/dist/cache-I72HKDOA.js.map +0 -1
- package/dist/chunk-ANE4PDC2.js.map +0 -1
- package/dist/chunk-DY5E3AT7.js.map +0 -1
- package/dist/chunk-MRRFJ6SA.js +0 -161
- package/dist/chunk-MRRFJ6SA.js.map +0 -1
- package/dist/chunk-UY7AM4QG.js.map +0 -1
- package/dist/chunk-WYB6MBZA.js.map +0 -1
- package/src/components/DataTable/__tests__/DataTable.autoSizing.test.tsx +0 -526
- package/src/components/DataTable/components/DataTableHeader.tsx +0 -31
- package/src/components/DataTable/components/__tests__/DataTableHeader.unit.test.tsx +0 -143
- package/src/components/DataTable/examples/AutoSizingExample.tsx +0 -180
- package/src/components/DataTable/examples/ColumnSizingComparison.tsx +0 -235
- package/src/components/DataTable/utils/__tests__/columnSizing.test.ts +0 -237
- package/src/components/DataTable/utils/columnSizing.ts +0 -125
- /package/dist/{DataTable-GEY5U7OI.js.map → DataTable-EEUDXPE5.js.map} +0 -0
- /package/dist/{api-T6CBS7IO.js.map → api-ETQ6YJ3C.js.map} +0 -0
- /package/dist/{chunk-TMRLB2LA.js.map → chunk-HEMJ4SUJ.js.map} +0 -0
- /package/dist/{chunk-O4T53L7X.js.map → chunk-HNDFPXUU.js.map} +0 -0
- /package/dist/{chunk-PFRRIDYA.js.map → chunk-TIVL4UQ7.js.map} +0 -0
- /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
|
-
|
|
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
|
|
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: ()
|
|
8
|
+
useUnifiedAuth: vi.fn()
|
|
13
9
|
}));
|
|
14
10
|
|
|
15
11
|
vi.mock('../../providers/OrganisationProvider', () => ({
|
|
16
|
-
useOrganisations: ()
|
|
12
|
+
useOrganisations: vi.fn()
|
|
17
13
|
}));
|
|
18
14
|
|
|
19
15
|
vi.mock('../../providers/EventProvider', () => ({
|
|
20
|
-
useEvents: ()
|
|
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
|
-
//
|
|
84
|
-
|
|
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
|
-
|
|
91
|
+
vi.mocked(useOrganisations).mockReturnValue({
|
|
92
92
|
selectedOrganisation: { id: 'test-org-id' }
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/styles/core.css
CHANGED
|
@@ -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;
|
|
101
|
-
|
|
102
|
-
--print-
|
|
103
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 {
|
|
311
|
-
|
|
312
|
-
|
|
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 {
|
|
361
|
-
|
|
362
|
-
|
|
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 {
|