@jmruthers/pace-core 0.5.105 → 0.5.107

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 (159) hide show
  1. package/dist/{DataTable-BE0OXZKQ.d.ts → DataTable-D5cBRca8.d.ts} +1 -1
  2. package/dist/{DataTable-LWHFLTEW.js → DataTable-H2WIR2DN.js} +3 -3
  3. package/dist/{chunk-QPCAGLUS.js → chunk-4OX5PXHX.js} +5 -2
  4. package/dist/chunk-4OX5PXHX.js.map +1 -0
  5. package/dist/{chunk-75G3NZWN.js → chunk-5JJCXTVE.js} +293 -37
  6. package/dist/chunk-5JJCXTVE.js.map +1 -0
  7. package/dist/{chunk-HBGPLSA5.js → chunk-DMNMZKWS.js} +70 -24
  8. package/dist/chunk-DMNMZKWS.js.map +1 -0
  9. package/dist/{chunk-AZFPGDCJ.js → chunk-EWKCROSF.js} +133 -49
  10. package/dist/chunk-EWKCROSF.js.map +1 -0
  11. package/dist/{chunk-4BWGRQBG.js → chunk-NFPV7MRN.js} +22 -2
  12. package/dist/chunk-NFPV7MRN.js.map +1 -0
  13. package/dist/{chunk-DWYMGSGU.js → chunk-VJ7MPS2K.js} +2 -2
  14. package/dist/components.d.ts +3 -3
  15. package/dist/components.js +4 -4
  16. package/dist/{formatting-BfDeV-ja.d.ts → formatting-BiEv5oEk.d.ts} +32 -2
  17. package/dist/hooks.d.ts +2 -2
  18. package/dist/hooks.js +3 -3
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.js +6 -6
  21. package/dist/{types-BDg1mAGG.d.ts → types-D4TVpDa1.d.ts} +24 -1
  22. package/dist/{useToast-Bm6TnSK-.d.ts → useToast-DRah6K-g.d.ts} +5 -2
  23. package/dist/utils.d.ts +3 -3
  24. package/dist/utils.js +2 -2
  25. package/docs/api/classes/ColumnFactory.md +1 -1
  26. package/docs/api/classes/ErrorBoundary.md +1 -1
  27. package/docs/api/classes/InvalidScopeError.md +1 -1
  28. package/docs/api/classes/MissingUserContextError.md +1 -1
  29. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  30. package/docs/api/classes/PermissionDeniedError.md +1 -1
  31. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  32. package/docs/api/classes/RBACAuditManager.md +1 -1
  33. package/docs/api/classes/RBACCache.md +1 -1
  34. package/docs/api/classes/RBACEngine.md +1 -1
  35. package/docs/api/classes/RBACError.md +1 -1
  36. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  37. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  38. package/docs/api/classes/StorageUtils.md +1 -1
  39. package/docs/api/enums/FileCategory.md +1 -1
  40. package/docs/api/interfaces/AggregateConfig.md +4 -4
  41. package/docs/api/interfaces/ButtonProps.md +1 -1
  42. package/docs/api/interfaces/CardProps.md +1 -1
  43. package/docs/api/interfaces/ColorPalette.md +1 -1
  44. package/docs/api/interfaces/ColorShade.md +1 -1
  45. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  46. package/docs/api/interfaces/DataRecord.md +1 -1
  47. package/docs/api/interfaces/DataTableAction.md +18 -18
  48. package/docs/api/interfaces/DataTableColumn.md +115 -10
  49. package/docs/api/interfaces/DataTableProps.md +38 -38
  50. package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
  51. package/docs/api/interfaces/EmptyStateConfig.md +5 -5
  52. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  53. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  54. package/docs/api/interfaces/FileMetadata.md +1 -1
  55. package/docs/api/interfaces/FileReference.md +1 -1
  56. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  57. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  58. package/docs/api/interfaces/FileUploadProps.md +1 -1
  59. package/docs/api/interfaces/FooterProps.md +1 -1
  60. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  61. package/docs/api/interfaces/InputProps.md +1 -1
  62. package/docs/api/interfaces/LabelProps.md +1 -1
  63. package/docs/api/interfaces/LoginFormProps.md +1 -1
  64. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  65. package/docs/api/interfaces/NavigationContextType.md +1 -1
  66. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  67. package/docs/api/interfaces/NavigationItem.md +1 -1
  68. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  69. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  70. package/docs/api/interfaces/Organisation.md +1 -1
  71. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  72. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  73. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  74. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  75. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  76. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  77. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  78. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  79. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  80. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  81. package/docs/api/interfaces/PaletteData.md +1 -1
  82. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  83. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  84. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  85. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  86. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  88. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  89. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  90. package/docs/api/interfaces/RBACConfig.md +1 -1
  91. package/docs/api/interfaces/RBACLogger.md +1 -1
  92. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  93. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  94. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  95. package/docs/api/interfaces/RouteConfig.md +1 -1
  96. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  97. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  98. package/docs/api/interfaces/StorageConfig.md +1 -1
  99. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  100. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  101. package/docs/api/interfaces/StorageListOptions.md +1 -1
  102. package/docs/api/interfaces/StorageListResult.md +1 -1
  103. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  104. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  105. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  106. package/docs/api/interfaces/StyleImport.md +1 -1
  107. package/docs/api/interfaces/SwitchProps.md +1 -1
  108. package/docs/api/interfaces/ToastActionElement.md +1 -1
  109. package/docs/api/interfaces/ToastProps.md +1 -1
  110. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  111. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  112. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  113. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  114. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  115. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  116. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  117. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  119. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  120. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  121. package/docs/api/interfaces/UserEventAccess.md +1 -1
  122. package/docs/api/interfaces/UserMenuProps.md +1 -1
  123. package/docs/api/interfaces/UserProfile.md +1 -1
  124. package/docs/api/modules.md +39 -18
  125. package/docs/api-reference/utilities.md +26 -3
  126. package/docs/implementation-guides/data-tables.md +390 -0
  127. package/package.json +1 -1
  128. package/src/components/DataTable/DataTable.tsx +4 -0
  129. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +25 -10
  130. package/src/components/DataTable/components/EditableRow.tsx +174 -16
  131. package/src/components/DataTable/components/UnifiedTableBody.tsx +205 -35
  132. package/src/components/DataTable/types.ts +34 -4
  133. package/src/components/FileDisplay/FileDisplay.test.tsx +184 -201
  134. package/src/components/FileDisplay/FileDisplay.tsx +40 -39
  135. package/src/components/NavigationMenu/NavigationMenu.test.tsx +189 -13
  136. package/src/components/NavigationMenu/NavigationMenu.tsx +142 -35
  137. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +4 -4
  138. package/src/components/Toast/Toast.tsx +1 -1
  139. package/src/hooks/public/usePublicFileDisplay.ts +25 -15
  140. package/src/hooks/useEventTheme.test.ts +11 -0
  141. package/src/hooks/useFileDisplay.ts +11 -0
  142. package/src/hooks/useSecureDataAccess.test.ts +22 -5
  143. package/src/hooks/useToast.ts +11 -2
  144. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +67 -3
  145. package/src/providers/__tests__/ProviderLifecycle.test.tsx +72 -4
  146. package/src/services/__tests__/OrganisationService.pagination.test.ts +10 -2
  147. package/src/styles/core.css +11 -0
  148. package/src/utils/__tests__/formatting.unit.test.ts +33 -0
  149. package/src/utils/file-reference.test.ts +44 -5
  150. package/src/utils/file-reference.ts +49 -26
  151. package/src/utils/formatting.ts +57 -2
  152. package/src/validation/__tests__/passwordSchema.unit.test.ts +3 -3
  153. package/dist/chunk-4BWGRQBG.js.map +0 -1
  154. package/dist/chunk-75G3NZWN.js.map +0 -1
  155. package/dist/chunk-AZFPGDCJ.js.map +0 -1
  156. package/dist/chunk-HBGPLSA5.js.map +0 -1
  157. package/dist/chunk-QPCAGLUS.js.map +0 -1
  158. /package/dist/{DataTable-LWHFLTEW.js.map → DataTable-H2WIR2DN.js.map} +0 -0
  159. /package/dist/{chunk-DWYMGSGU.js.map → chunk-VJ7MPS2K.js.map} +0 -0
@@ -21,9 +21,11 @@ const mockAuthContext = {
21
21
  refreshSession: vi.fn(),
22
22
  appName: 'Test App',
23
23
  hasErrors: false,
24
- selectedOrganisation: null,
24
+ selectedOrganisation: { id: 'test-org-1', name: 'Test Org', display_name: 'Test Organisation', subscription_tier: 'basic', settings: {}, is_active: true, parent_id: null, created_at: '', updated_at: '' },
25
25
  organisations: [],
26
+ selectedEvent: null,
26
27
  events: [],
28
+ supabase: {},
27
29
  // Note: hasPermission, hasRole, hasAccessLevel, permissions, roles, and accessLevel
28
30
  // were removed from UnifiedAuthProvider. Use useRBAC() hook for permissions instead.
29
31
  // Inactivity context
@@ -39,6 +41,50 @@ vi.mock('../../providers/UnifiedAuthProvider', () => ({
39
41
  useUnifiedAuth: vi.fn(() => mockAuthContext),
40
42
  }));
41
43
 
44
+ // Mock RBAC hooks with hoisted mocks so they can be controlled per test
45
+ const mockUseRBAC = vi.hoisted(() => vi.fn(() => ({
46
+ user: { id: 'test-user', email: 'test@example.com' },
47
+ globalRole: null,
48
+ organisationRole: null,
49
+ eventAppRole: null,
50
+ hasPermission: vi.fn(),
51
+ hasGlobalPermission: vi.fn(),
52
+ isSuperAdmin: false,
53
+ isOrgAdmin: false,
54
+ isEventAdmin: false,
55
+ canManageOrganisation: false,
56
+ canManageEvent: false,
57
+ isLoading: false,
58
+ error: null,
59
+ })));
60
+
61
+ const mockUseResolvedScope = vi.hoisted(() => vi.fn(() => ({
62
+ resolvedScope: { organisationId: 'test-org-1', eventId: undefined, appId: undefined },
63
+ isLoading: false,
64
+ })));
65
+
66
+ const mockUsePermissions = vi.hoisted(() => vi.fn(() => ({
67
+ permissions: {} as any,
68
+ isLoading: false,
69
+ error: null,
70
+ hasPermission: vi.fn(() => false),
71
+ hasAnyPermission: vi.fn(() => false),
72
+ hasAllPermissions: vi.fn(() => false),
73
+ refetch: vi.fn(),
74
+ })));
75
+
76
+ vi.mock('../../rbac/hooks/useRBAC', () => ({
77
+ useRBAC: mockUseRBAC,
78
+ }));
79
+
80
+ vi.mock('../../rbac/hooks', () => ({
81
+ useResolvedScope: mockUseResolvedScope,
82
+ }));
83
+
84
+ vi.mock('../../rbac/hooks/usePermissions', () => ({
85
+ usePermissions: mockUsePermissions,
86
+ }));
87
+
42
88
  // Mock console methods to avoid noise in tests
43
89
  const originalConsoleLog = console.log;
44
90
  const originalConsoleWarn = console.warn;
@@ -381,15 +427,41 @@ describe('NavigationMenu Component', () => {
381
427
  // Permission-based filtering tests
382
428
  describe('Permission-Based Filtering', () => {
383
429
  beforeEach(() => {
384
- // Note: Permission checks are currently disabled in NavigationMenu
385
- // until migrated to useRBAC() hook. See NavigationMenu.tsx for TODOs.
386
430
  vi.clearAllMocks();
387
431
  });
388
432
 
389
- it.skip('renders items with permission requirements', async () => {
390
- // TODO: This test is skipped because NavigationMenu permission filtering is temporarily disabled
391
- // until migrated to useRBAC() hook. See NavigationMenu.tsx for TODOs.
433
+ it('renders items with permission requirements', async () => {
392
434
  const user = userEvent.setup();
435
+
436
+ // Mock RBAC hooks to grant permissions and roles
437
+ mockUseRBAC.mockReturnValue({
438
+ user: { id: 'test-user', email: 'test@example.com' },
439
+ globalRole: null,
440
+ organisationRole: 'org_admin' as any,
441
+ eventAppRole: 'event_admin' as any,
442
+ hasPermission: vi.fn(),
443
+ hasGlobalPermission: vi.fn(),
444
+ isSuperAdmin: false,
445
+ isOrgAdmin: true,
446
+ isEventAdmin: true,
447
+ canManageOrganisation: true,
448
+ canManageEvent: true,
449
+ isLoading: false,
450
+ error: null,
451
+ });
452
+
453
+ mockUsePermissions.mockReturnValue({
454
+ permissions: {
455
+ 'dashboard:read': true,
456
+ } as any,
457
+ isLoading: false,
458
+ error: null,
459
+ hasPermission: vi.fn((p: any) => p === 'dashboard:read'),
460
+ hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => p === 'dashboard:read')),
461
+ hasAllPermissions: vi.fn(() => true),
462
+ refetch: vi.fn(),
463
+ });
464
+
393
465
  renderWithProviders(
394
466
  <NavigationMenu
395
467
  items={permissionBasedNavItems}
@@ -507,11 +579,38 @@ describe('NavigationMenu Component', () => {
507
579
  expect(window.location.href).toBe('/');
508
580
  });
509
581
 
510
- it.skip('handles permission-based navigation', async () => {
511
- // TODO: This test is skipped because NavigationMenu permission filtering is temporarily disabled
512
- // until migrated to useRBAC() hook. See NavigationMenu.tsx for TODOs.
582
+ it('handles permission-based navigation', async () => {
513
583
  const user = userEvent.setup();
514
584
 
585
+ // Mock RBAC hooks to grant permissions and roles
586
+ mockUseRBAC.mockReturnValue({
587
+ user: { id: 'test-user', email: 'test@example.com' },
588
+ globalRole: null,
589
+ organisationRole: 'org_admin' as any,
590
+ eventAppRole: 'event_admin' as any,
591
+ hasPermission: vi.fn(),
592
+ hasGlobalPermission: vi.fn(),
593
+ isSuperAdmin: false,
594
+ isOrgAdmin: true,
595
+ isEventAdmin: true,
596
+ canManageOrganisation: true,
597
+ canManageEvent: true,
598
+ isLoading: false,
599
+ error: null,
600
+ });
601
+
602
+ mockUsePermissions.mockReturnValue({
603
+ permissions: {
604
+ 'dashboard:read': true,
605
+ } as any,
606
+ isLoading: false,
607
+ error: null,
608
+ hasPermission: vi.fn((p: any) => p === 'dashboard:read'),
609
+ hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => p === 'dashboard:read')),
610
+ hasAllPermissions: vi.fn(() => true),
611
+ refetch: vi.fn(),
612
+ });
613
+
515
614
  renderWithProviders(
516
615
  <NavigationMenu
517
616
  items={permissionBasedNavItems}
@@ -530,12 +629,52 @@ describe('NavigationMenu Component', () => {
530
629
  expect(screen.getByText('Users')).toBeInTheDocument();
531
630
  expect(screen.getByText('Settings')).toBeInTheDocument();
532
631
  }, { interval: 10 });
632
+
633
+ // Click on an item to verify navigation works
634
+ const dashboardItem = screen.getByText('Dashboard');
635
+ await user.click(dashboardItem);
636
+
637
+ expect(mockNavigate).toHaveBeenCalledWith(
638
+ expect.objectContaining({
639
+ id: 'dashboard',
640
+ label: 'Dashboard',
641
+ href: '/dashboard',
642
+ })
643
+ );
533
644
  });
534
645
 
535
- it.skip('handles strict mode configuration', async () => {
536
- // TODO: This test is skipped because NavigationMenu permission filtering is temporarily disabled
537
- // until migrated to useRBAC() hook. See NavigationMenu.tsx for TODOs.
646
+ it('handles strict mode configuration', async () => {
538
647
  const user = userEvent.setup();
648
+
649
+ // Mock RBAC hooks to grant permissions and roles
650
+ mockUseRBAC.mockReturnValue({
651
+ user: { id: 'test-user', email: 'test@example.com' },
652
+ globalRole: null,
653
+ organisationRole: 'org_admin' as any,
654
+ eventAppRole: 'event_admin' as any,
655
+ hasPermission: vi.fn(),
656
+ hasGlobalPermission: vi.fn(),
657
+ isSuperAdmin: false,
658
+ isOrgAdmin: true,
659
+ isEventAdmin: true,
660
+ canManageOrganisation: true,
661
+ canManageEvent: true,
662
+ isLoading: false,
663
+ error: null,
664
+ });
665
+
666
+ mockUsePermissions.mockReturnValue({
667
+ permissions: {
668
+ 'dashboard:read': true,
669
+ } as any,
670
+ isLoading: false,
671
+ error: null,
672
+ hasPermission: vi.fn((p: any) => p === 'dashboard:read'),
673
+ hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => p === 'dashboard:read')),
674
+ hasAllPermissions: vi.fn(() => true),
675
+ refetch: vi.fn(),
676
+ });
677
+
539
678
  renderWithProviders(
540
679
  <NavigationMenu
541
680
  items={permissionBasedNavItems}
@@ -555,6 +694,14 @@ describe('NavigationMenu Component', () => {
555
694
  expect(screen.getByText('Users')).toBeInTheDocument();
556
695
  expect(screen.getByText('Settings')).toBeInTheDocument();
557
696
  }, { interval: 10 });
697
+
698
+ // Verify strict mode is working - navigation should proceed with permissions
699
+ const dashboardItem = screen.getByText('Dashboard');
700
+ await user.click(dashboardItem);
701
+
702
+ expect(mockNavigate).toHaveBeenCalled();
703
+ // Should not trigger strict mode violation since user has permissions
704
+ expect(mockOnStrictModeViolation).not.toHaveBeenCalled();
558
705
  });
559
706
  });
560
707
 
@@ -693,6 +840,35 @@ describe('NavigationMenu Component', () => {
693
840
  },
694
841
  ];
695
842
 
843
+ // Mock RBAC to grant the valid permission
844
+ mockUseRBAC.mockReturnValue({
845
+ user: { id: 'test-user', email: 'test@example.com' },
846
+ globalRole: null,
847
+ organisationRole: null,
848
+ eventAppRole: null,
849
+ hasPermission: vi.fn(),
850
+ hasGlobalPermission: vi.fn(),
851
+ isSuperAdmin: false,
852
+ isOrgAdmin: false,
853
+ isEventAdmin: false,
854
+ canManageOrganisation: false,
855
+ canManageEvent: false,
856
+ isLoading: false,
857
+ error: null,
858
+ });
859
+
860
+ mockUsePermissions.mockReturnValue({
861
+ permissions: {
862
+ 'valid-permission': true,
863
+ } as any,
864
+ isLoading: false,
865
+ error: null,
866
+ hasPermission: vi.fn((p: any) => p === 'valid-permission'),
867
+ hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => typeof p === 'string' && p === 'valid-permission')),
868
+ hasAllPermissions: vi.fn(() => true),
869
+ refetch: vi.fn(),
870
+ });
871
+
696
872
  renderWithProviders(
697
873
  <NavigationMenu
698
874
  items={invalidPermissionItems}
@@ -706,7 +882,7 @@ describe('NavigationMenu Component', () => {
706
882
  await user.click(trigger);
707
883
 
708
884
  await waitFor(() => {
709
- // Should not crash and should show the item
885
+ // Should not crash and should show the item (invalid types filtered out, valid permission granted)
710
886
  expect(screen.getByText('Test')).toBeInTheDocument();
711
887
  }, { interval: 10 });
712
888
  });
@@ -198,7 +198,10 @@ import { NavigationMenuProps, NavigationItem } from "./types";
198
198
  import { Button } from "../Button";
199
199
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../Select";
200
200
  import { useUnifiedAuth } from "../../providers/UnifiedAuthProvider";
201
- import { AccessLevel } from "../../types/unified";
201
+ import { useRBAC } from "../../rbac/hooks/useRBAC";
202
+ import { useResolvedScope } from "../../rbac/hooks";
203
+ import { usePermissions } from "../../rbac/hooks/usePermissions";
204
+ import type { Permission, AccessLevel as RBACAccessLevel } from "../../rbac/types";
202
205
 
203
206
  /**
204
207
  * Unified NavigationMenu component that supports both dropdown and hierarchical navigation modes.
@@ -424,10 +427,40 @@ export const NavigationMenu = React.forwardRef<
424
427
  console.warn('[NavigationMenu] useUnifiedAuth not available, running in unauthenticated mode');
425
428
  }
426
429
 
430
+ // Get RBAC context for permission and role checks
431
+ let rbacContext = null;
432
+ try {
433
+ rbacContext = useRBAC();
434
+ } catch (error) {
435
+ // RBAC not available - permission filtering will be disabled
436
+ console.warn('[NavigationMenu] useRBAC not available, permission filtering disabled');
437
+ }
438
+
439
+ // Get resolved scope for permission checks
440
+ const { supabase } = authContext || {};
441
+ const { selectedOrganisation } = authContext || {};
442
+ const selectedEvent = authContext?.selectedEvent || null;
443
+ const { resolvedScope, isLoading: scopeLoading } = useResolvedScope({
444
+ supabase: supabase || null,
445
+ selectedOrganisationId: selectedOrganisation?.id || null,
446
+ selectedEventId: selectedEvent?.event_id || null
447
+ });
448
+
449
+ // Get permissions map for synchronous permission checks
450
+ const userId = authContext?.user?.id || '';
451
+ const scope = resolvedScope || { organisationId: '', eventId: undefined, appId: undefined };
452
+ const { permissions: permissionMap, hasAnyPermission, isLoading: permissionsLoading } = usePermissions(
453
+ userId as any,
454
+ scope as any
455
+ );
456
+
427
457
  // NEW: Phase 2 - Enhanced Security Features
428
- // Filter navigation items based on permissions
458
+ // Filter navigation items based on permissions using RBAC hooks
429
459
  const filteredItems = React.useMemo(() => {
430
- if (!filterByPermissions || !authContext) return items || [];
460
+ // If filtering disabled, auth unavailable, RBAC unavailable, scope/permissions loading, or no org context: show all items (except hidden)
461
+ if (!filterByPermissions || !authContext || !rbacContext || scopeLoading || permissionsLoading || !resolvedScope?.organisationId) {
462
+ return (items || []).filter(item => !item.meta?.hidden);
463
+ }
431
464
 
432
465
  return (items || []).filter(item => {
433
466
  // Check if item should be hidden
@@ -435,44 +468,98 @@ export const NavigationMenu = React.forwardRef<
435
468
 
436
469
  // Check permissions if available
437
470
  if (item.permissions && item.permissions.length > 0) {
438
- const hasPermission = item.permissions.some(permission => {
439
- // Only check string permissions, ignore invalid types
440
- if (typeof permission !== 'string') return true;
441
- // TODO: Migrate to useRBAC() hook for permission checks
442
- // RBAC properties were removed from UnifiedAuthProvider
443
- return false; // Default to no permission until migrated
444
- });
445
- if (!hasPermission) return false;
471
+ // Convert string permissions to Permission type and check
472
+ const permissions = item.permissions
473
+ .filter((p): p is string => typeof p === 'string')
474
+ .map(p => p as Permission);
475
+
476
+ if (permissions.length > 0) {
477
+ const hasPermission = hasAnyPermission(permissions);
478
+ if (!hasPermission) return false;
479
+ }
446
480
  }
447
481
 
448
482
  // Check roles if available
449
483
  if (item.roles && item.roles.length > 0) {
450
484
  const hasRole = item.roles.some(role => {
451
- // Only check string roles, ignore invalid types
452
485
  if (typeof role !== 'string') return true;
453
- // TODO: Migrate to useRBAC() hook for role checks
454
- // RBAC properties were removed from UnifiedAuthProvider
455
- return false; // Default to no role until migrated
486
+
487
+ // Map role strings to RBAC role checks
488
+ switch (role.toLowerCase()) {
489
+ case 'super_admin':
490
+ case 'super admin':
491
+ return rbacContext.isSuperAdmin;
492
+ case 'org_admin':
493
+ case 'org admin':
494
+ case 'admin':
495
+ return rbacContext.isOrgAdmin || rbacContext.isSuperAdmin;
496
+ case 'event_admin':
497
+ case 'event admin':
498
+ return rbacContext.isEventAdmin || rbacContext.isSuperAdmin;
499
+ default:
500
+ // For other roles, check against organisationRole or eventAppRole
501
+ return (
502
+ rbacContext.organisationRole === role ||
503
+ rbacContext.eventAppRole === role ||
504
+ rbacContext.isSuperAdmin
505
+ );
506
+ }
456
507
  });
457
508
  if (!hasRole) return false;
458
509
  }
459
510
 
460
511
  // Check access level if available
461
512
  if (item.accessLevel) {
462
- // Only check string access levels, ignore invalid types
463
513
  if (typeof item.accessLevel === 'string') {
464
- // Convert string to AccessLevel enum
465
- const accessLevel = item.accessLevel as AccessLevel;
466
- // TODO: Migrate to useRBAC() hook for access level checks
467
- // RBAC properties were removed from UnifiedAuthProvider
468
- const hasAccessLevel = false; // Default to no access until migrated
469
- if (!hasAccessLevel) return false;
514
+ // Map access level string to RBAC access level
515
+ const accessLevel = item.accessLevel.toLowerCase() as RBACAccessLevel;
516
+ const userEventRole = rbacContext.eventAppRole;
517
+
518
+ // If user is super admin, they have all access levels
519
+ if (rbacContext.isSuperAdmin) {
520
+ // Super admin has access
521
+ } else {
522
+ // Map eventAppRole to access level for comparison
523
+ // eventAppRole: 'viewer' | 'participant' | 'planner' | 'event_admin'
524
+ const roleToAccessLevel: Record<string, RBACAccessLevel> = {
525
+ 'viewer': 'viewer',
526
+ 'participant': 'participant',
527
+ 'planner': 'planner',
528
+ 'event_admin': 'admin',
529
+ };
530
+ const userAccessLevel = userEventRole ? (roleToAccessLevel[userEventRole] || 'viewer') : null;
531
+
532
+ // Check if user's access level meets the required access level
533
+ const levelHierarchy: Record<RBACAccessLevel, number> = {
534
+ viewer: 1,
535
+ participant: 2,
536
+ planner: 3,
537
+ admin: 4,
538
+ super: 5
539
+ };
540
+ const requiredLevel = levelHierarchy[accessLevel] || 0;
541
+ const userLevel = userAccessLevel ? levelHierarchy[userAccessLevel] || 0 : 0;
542
+
543
+ if (userLevel < requiredLevel) {
544
+ return false;
545
+ }
546
+ }
470
547
  }
471
548
  }
472
549
 
473
550
  return true;
474
551
  });
475
- }, [items, filterByPermissions, authContext]);
552
+ }, [
553
+ items,
554
+ filterByPermissions,
555
+ authContext,
556
+ rbacContext,
557
+ permissionMap,
558
+ hasAnyPermission,
559
+ scopeLoading,
560
+ permissionsLoading,
561
+ resolvedScope
562
+ ]);
476
563
 
477
564
  // Log navigation access attempts for debugging
478
565
  React.useEffect(() => {
@@ -552,26 +639,46 @@ export const NavigationMenu = React.forwardRef<
552
639
  // Check if item should be visible (already filtered)
553
640
  const isItemVisible = filteredItems.some(filtered => filtered.id === item.id);
554
641
 
555
- // Check permissions if the item requires them
642
+ // Check permissions if the item requires them using RBAC hooks
556
643
  let hasPermission = true; // Default to true if no permission requirements
557
644
 
558
- if (item.permissions && item.permissions.length > 0) {
559
- hasPermission = item.permissions.some(permission => {
560
- if (typeof permission !== 'string') return true;
561
- // TODO: Migrate to useRBAC() hook for permission checks
562
- // RBAC properties were removed from UnifiedAuthProvider
563
- return false; // Default to no permission until migrated
564
- });
645
+ if (item.permissions && item.permissions.length > 0 && rbacContext && hasAnyPermission) {
646
+ // Convert string permissions to Permission type and check
647
+ const permissions = item.permissions
648
+ .filter((p): p is string => typeof p === 'string')
649
+ .map(p => p as Permission);
650
+
651
+ if (permissions.length > 0) {
652
+ hasPermission = hasAnyPermission(permissions);
653
+ }
565
654
  }
566
655
 
567
- if (!hasPermission) {
656
+ if (!hasPermission && rbacContext) {
568
657
  // Check roles if permissions check failed or item has role requirements
569
658
  if (item.roles && item.roles.length > 0) {
570
659
  hasPermission = item.roles.some(role => {
571
660
  if (typeof role !== 'string') return true;
572
- // TODO: Migrate to useRBAC() hook for role checks
573
- // RBAC properties were removed from UnifiedAuthProvider
574
- return false; // Default to no role until migrated
661
+
662
+ // Map role strings to RBAC role checks (same logic as filtering)
663
+ switch (role.toLowerCase()) {
664
+ case 'super_admin':
665
+ case 'super admin':
666
+ return rbacContext.isSuperAdmin;
667
+ case 'org_admin':
668
+ case 'org admin':
669
+ case 'admin':
670
+ return rbacContext.isOrgAdmin || rbacContext.isSuperAdmin;
671
+ case 'event_admin':
672
+ case 'event admin':
673
+ return rbacContext.isEventAdmin || rbacContext.isSuperAdmin;
674
+ default:
675
+ // For other roles, check against organisationRole or eventAppRole
676
+ return (
677
+ rbacContext.organisationRole === role ||
678
+ rbacContext.eventAppRole === role ||
679
+ rbacContext.isSuperAdmin
680
+ );
681
+ }
575
682
  });
576
683
  }
577
684
  }
@@ -48,12 +48,12 @@ vi.mock('../../../hooks/useAppConfig', () => ({
48
48
 
49
49
  // Mock the FileDisplay component
50
50
  vi.mock('../../FileDisplay/FileDisplay', () => ({
51
- FileDisplay: vi.fn(({ table_name, record_id, organisation_id, category, className }) => (
51
+ FileDisplay: vi.fn(({ table_name, record_id, organisation_id, category, className, size = 'md' }) => (
52
52
  <div
53
53
  data-testid="event-logo"
54
- data-event-id={eventId}
55
- data-event-name={eventName}
56
- data-organisation-id={organisationId}
54
+ data-table-name={table_name}
55
+ data-record-id={record_id}
56
+ data-organisation-id={organisation_id}
57
57
  data-size={size}
58
58
  className={className}
59
59
  >
@@ -12,7 +12,7 @@
12
12
  * - Customizable positioning and styling
13
13
  * - Swipe gestures for dismissal
14
14
  * - Keyboard navigation support
15
- * - Auto-dismiss with configurable duration
15
+ * - Auto-dismiss with default 10 second duration (configurable)
16
16
  * - Action buttons and close functionality
17
17
  * - Responsive design
18
18
  * - Accessibility compliant
@@ -169,24 +169,34 @@ export function usePublicFileDisplay(
169
169
  throw new Error(rpcError.message || 'Failed to fetch file reference');
170
170
  }
171
171
 
172
- // RPC returns partial data, need to fetch full file references
172
+ // RPC returns partial data with: id, file_path, file_metadata, is_public, created_at
173
+ // We have table_name, record_id, organisation_id from function parameters
174
+ // We can construct FileReference objects directly without another query (avoiding RLS issues)
173
175
  if (!data || data.length === 0) {
174
176
  files = [];
175
177
  } else {
176
- // Extract IDs from RPC response and fetch full file reference data
177
- // Note: This query does NOT filter by category - the RPC already did that filtering
178
- const ids = data.map((item: any) => item.id);
179
- const { data: fullData, error: fetchError } = await supabase
180
- .from('file_references')
181
- .select('*')
182
- .in('id', ids)
183
- .eq('is_public', true); // Only public files in public context
184
-
185
- if (fetchError) {
186
- throw new Error(fetchError.message || 'Failed to fetch file references');
187
- }
188
-
189
- files = fullData || [];
178
+ // Construct file reference objects from RPC response
179
+ // This avoids RLS issues - the RPC already validated permissions and filtered public files
180
+ files = data
181
+ .filter((item: any) => {
182
+ // RPC should only return public files for public context, but verify
183
+ return item.is_public === true && item.id && item.file_path && item.file_metadata;
184
+ })
185
+ .map((item: any) => {
186
+ // Construct complete file reference from RPC response + function parameters
187
+ return {
188
+ id: item.id,
189
+ table_name: table_name,
190
+ record_id: record_id,
191
+ file_path: item.file_path,
192
+ file_metadata: item.file_metadata || {},
193
+ organisation_id: organisation_id,
194
+ app_id: item.file_metadata?.app_id || null,
195
+ is_public: true, // RPC already filtered for public files
196
+ created_at: item.created_at || new Date().toISOString(),
197
+ updated_at: item.created_at || new Date().toISOString()
198
+ };
199
+ });
190
200
  }
191
201
  } else {
192
202
  // Multiple files mode - use RPC to get all files
@@ -23,6 +23,17 @@ vi.mock('../theming/runtime', () => ({
23
23
  clearPalette: vi.fn()
24
24
  }));
25
25
 
26
+ // Mock react-router-dom useLocation
27
+ vi.mock('react-router-dom', () => ({
28
+ useLocation: vi.fn(() => ({
29
+ pathname: '/',
30
+ search: '',
31
+ hash: '',
32
+ state: null,
33
+ key: 'default'
34
+ }))
35
+ }));
36
+
26
37
  describe('useEventTheme', () => {
27
38
  const mockUseEvents = vi.mocked(useEvents);
28
39
  const mockApplyPalette = vi.mocked(applyPalette);
@@ -224,12 +224,21 @@ export function useFileDisplay(
224
224
  if (category && files.length > 0) {
225
225
  // Single file mode - get first file
226
226
  const firstFile = files[0];
227
+ console.log('[useFileDisplay] Processing category files, first file:', {
228
+ id: firstFile.id,
229
+ file_path: firstFile.file_path,
230
+ is_public: firstFile.is_public,
231
+ has_file_metadata: !!firstFile.file_metadata,
232
+ category_in_metadata: firstFile.file_metadata?.category
233
+ });
234
+
227
235
  setFileReference(firstFile);
228
236
 
229
237
  // Generate URL based on file visibility
230
238
  let url: string | null = null;
231
239
  if (firstFile.is_public) {
232
240
  url = getPublicUrl(supabase, firstFile.file_path, true);
241
+ console.log('[useFileDisplay] Generated public URL:', url);
233
242
  } else {
234
243
  const signedUrlResult = await getSignedUrl(supabase, firstFile.file_path, {
235
244
  appName: 'pace-core',
@@ -237,7 +246,9 @@ export function useFileDisplay(
237
246
  expiresIn: 3600
238
247
  });
239
248
  url = signedUrlResult?.url || null;
249
+ console.log('[useFileDisplay] Generated signed URL:', url ? 'URL generated' : 'URL generation failed');
240
250
  }
251
+ console.log('[useFileDisplay] Setting file URL:', url ? 'URL set' : 'URL is null');
241
252
  setFileUrl(url);
242
253
  } else {
243
254
  // Multiple files mode - generate URLs for all files
@@ -152,12 +152,28 @@ describe('useSecureDataAccess', () => {
152
152
  expect(freshMockQueryBuilder.select).toHaveBeenCalledWith('*');
153
153
  });
154
154
 
155
- it.skip('executes secure query with pagination', async () => {
156
- // Skipped: Complex mock setup for range() chaining requires additional work
157
- // The mock needs to properly support: .from().select().eq().eq().range().then()
158
- // This is a complex integration test that would require refactoring the Supabase mock
159
- // See issue: Need comprehensive range() support in createMockQueryBuilder
155
+ it('executes secure query with pagination', async () => {
156
+ const paginatedData = Array.from({ length: 10 }, (_, i) => ({ id: `record-${i + 20}` }));
160
157
 
158
+ // Ensure range() returns a thenable that resolves with paginated data
159
+ // and that all chain methods (eq, select, etc.) maintain the range function
160
+ freshMockQueryBuilder.range = vi.fn().mockImplementation(function(min: number, max: number) {
161
+ const rangedBuilder = Object.assign({}, this, {
162
+ then: vi.fn().mockImplementation((resolve) => {
163
+ resolve({ data: paginatedData, error: null });
164
+ }),
165
+ catch: vi.fn(),
166
+ finally: vi.fn(),
167
+ range: freshMockQueryBuilder.range // Maintain range for further chaining if needed
168
+ });
169
+ return rangedBuilder;
170
+ });
171
+
172
+ // Ensure eq() returns this which has range()
173
+ freshMockQueryBuilder.eq = vi.fn().mockReturnThis();
174
+ freshMockQueryBuilder.select = vi.fn().mockReturnThis();
175
+ freshMockQueryBuilder.limit = vi.fn().mockReturnThis();
176
+
161
177
  const { result } = renderHook(() => useSecureDataAccess());
162
178
 
163
179
  const data = await result.current.secureQuery('users', '*', {}, {
@@ -167,6 +183,7 @@ describe('useSecureDataAccess', () => {
167
183
 
168
184
  expect(freshMockQueryBuilder.select).toHaveBeenCalledWith('*');
169
185
  expect(freshMockQueryBuilder.range).toHaveBeenCalledWith(20, 29); // offset to (offset + limit - 1)
186
+ expect(data).toEqual(paginatedData);
170
187
  });
171
188
 
172
189
  it('handles query errors gracefully', async () => {