@jmruthers/pace-core 0.5.106 → 0.5.108

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 (156) hide show
  1. package/dist/{DataTable-BE0OXZKQ.d.ts → DataTable-D5cBRca8.d.ts} +1 -1
  2. package/dist/{DataTable-LWHFLTEW.js → DataTable-WFCHVWTY.js} +3 -3
  3. package/dist/{PublicLoadingSpinner-48ewSMKK.d.ts → PublicLoadingSpinner-DgDWTFqn.d.ts} +4 -2
  4. package/dist/{chunk-QPCAGLUS.js → chunk-4OX5PXHX.js} +5 -2
  5. package/dist/chunk-4OX5PXHX.js.map +1 -0
  6. package/dist/{chunk-IBZBNBTE.js → chunk-B3QX32P5.js} +177 -54
  7. package/dist/chunk-B3QX32P5.js.map +1 -0
  8. package/dist/{chunk-75G3NZWN.js → chunk-IMZGJ2X7.js} +373 -95
  9. package/dist/chunk-IMZGJ2X7.js.map +1 -0
  10. package/dist/{chunk-4BWGRQBG.js → chunk-NFPV7MRN.js} +22 -2
  11. package/dist/chunk-NFPV7MRN.js.map +1 -0
  12. package/dist/components.d.ts +4 -4
  13. package/dist/components.js +3 -3
  14. package/dist/{formatting-BfDeV-ja.d.ts → formatting-BiEv5oEk.d.ts} +32 -2
  15. package/dist/hooks.d.ts +2 -2
  16. package/dist/hooks.js +1 -1
  17. package/dist/index.d.ts +6 -6
  18. package/dist/index.js +4 -4
  19. package/dist/{types-BDg1mAGG.d.ts → types-D4TVpDa1.d.ts} +24 -1
  20. package/dist/{useToast-Bm6TnSK-.d.ts → useToast-DRah6K-g.d.ts} +5 -2
  21. package/dist/utils.d.ts +3 -3
  22. package/dist/utils.js +2 -2
  23. package/docs/api/classes/ColumnFactory.md +1 -1
  24. package/docs/api/classes/ErrorBoundary.md +1 -1
  25. package/docs/api/classes/InvalidScopeError.md +1 -1
  26. package/docs/api/classes/MissingUserContextError.md +1 -1
  27. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  28. package/docs/api/classes/PermissionDeniedError.md +1 -1
  29. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  30. package/docs/api/classes/RBACAuditManager.md +1 -1
  31. package/docs/api/classes/RBACCache.md +1 -1
  32. package/docs/api/classes/RBACEngine.md +1 -1
  33. package/docs/api/classes/RBACError.md +1 -1
  34. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  35. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  36. package/docs/api/classes/StorageUtils.md +1 -1
  37. package/docs/api/enums/FileCategory.md +1 -1
  38. package/docs/api/interfaces/AggregateConfig.md +4 -4
  39. package/docs/api/interfaces/ButtonProps.md +1 -1
  40. package/docs/api/interfaces/CardProps.md +1 -1
  41. package/docs/api/interfaces/ColorPalette.md +1 -1
  42. package/docs/api/interfaces/ColorShade.md +1 -1
  43. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  44. package/docs/api/interfaces/DataRecord.md +1 -1
  45. package/docs/api/interfaces/DataTableAction.md +18 -18
  46. package/docs/api/interfaces/DataTableColumn.md +115 -10
  47. package/docs/api/interfaces/DataTableProps.md +38 -38
  48. package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
  49. package/docs/api/interfaces/EmptyStateConfig.md +5 -5
  50. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  51. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  52. package/docs/api/interfaces/FileMetadata.md +1 -1
  53. package/docs/api/interfaces/FileReference.md +1 -1
  54. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  55. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  56. package/docs/api/interfaces/FileUploadProps.md +1 -1
  57. package/docs/api/interfaces/FooterProps.md +1 -1
  58. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  59. package/docs/api/interfaces/InputProps.md +1 -1
  60. package/docs/api/interfaces/LabelProps.md +1 -1
  61. package/docs/api/interfaces/LoginFormProps.md +1 -1
  62. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  63. package/docs/api/interfaces/NavigationContextType.md +1 -1
  64. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  65. package/docs/api/interfaces/NavigationItem.md +1 -1
  66. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  67. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  68. package/docs/api/interfaces/Organisation.md +1 -1
  69. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  70. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  71. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  72. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  73. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  74. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  75. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  76. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  77. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  78. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  79. package/docs/api/interfaces/PaletteData.md +1 -1
  80. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  81. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  82. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  83. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  84. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  85. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  86. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  88. package/docs/api/interfaces/RBACConfig.md +1 -1
  89. package/docs/api/interfaces/RBACLogger.md +1 -1
  90. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  91. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  92. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  93. package/docs/api/interfaces/RouteConfig.md +1 -1
  94. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  95. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  96. package/docs/api/interfaces/StorageConfig.md +1 -1
  97. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  98. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  99. package/docs/api/interfaces/StorageListOptions.md +1 -1
  100. package/docs/api/interfaces/StorageListResult.md +1 -1
  101. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  102. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  103. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  104. package/docs/api/interfaces/StyleImport.md +1 -1
  105. package/docs/api/interfaces/SwitchProps.md +1 -1
  106. package/docs/api/interfaces/ToastActionElement.md +1 -1
  107. package/docs/api/interfaces/ToastProps.md +1 -1
  108. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  109. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  110. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  111. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  112. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  113. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  114. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  115. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  116. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  117. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  118. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  119. package/docs/api/interfaces/UserEventAccess.md +1 -1
  120. package/docs/api/interfaces/UserMenuProps.md +1 -1
  121. package/docs/api/interfaces/UserProfile.md +1 -1
  122. package/docs/api/modules.md +42 -19
  123. package/docs/api-reference/utilities.md +26 -3
  124. package/docs/implementation-guides/data-tables.md +390 -0
  125. package/package.json +1 -1
  126. package/src/components/DataTable/DataTable.tsx +4 -0
  127. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +25 -10
  128. package/src/components/DataTable/components/ColumnFilter.tsx +2 -1
  129. package/src/components/DataTable/components/EditableRow.tsx +179 -16
  130. package/src/components/DataTable/components/FilterRow.tsx +22 -11
  131. package/src/components/DataTable/components/PaginationControls.tsx +1 -1
  132. package/src/components/DataTable/components/UnifiedTableBody.tsx +231 -32
  133. package/src/components/DataTable/types.ts +34 -4
  134. package/src/components/FileDisplay/FileDisplay.test.tsx +184 -201
  135. package/src/components/FileDisplay/FileDisplay.tsx +40 -39
  136. package/src/components/NavigationMenu/NavigationMenu.test.tsx +189 -13
  137. package/src/components/NavigationMenu/NavigationMenu.tsx +142 -35
  138. package/src/components/PaceAppLayout/PaceAppLayout.tsx +79 -10
  139. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +4 -4
  140. package/src/components/Toast/Toast.tsx +1 -1
  141. package/src/hooks/useEventTheme.test.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/formatting.ts +57 -2
  151. package/src/validation/__tests__/passwordSchema.unit.test.ts +3 -3
  152. package/dist/chunk-4BWGRQBG.js.map +0 -1
  153. package/dist/chunk-75G3NZWN.js.map +0 -1
  154. package/dist/chunk-IBZBNBTE.js.map +0 -1
  155. package/dist/chunk-QPCAGLUS.js.map +0 -1
  156. /package/dist/{DataTable-LWHFLTEW.js.map → DataTable-WFCHVWTY.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
  }
@@ -243,7 +243,7 @@ export interface PaceAppLayoutProps {
243
243
  *
244
244
  *
245
245
  * @example
246
- * Custom navigation items with permission filtering:
246
+ * Custom navigation items with permission filtering (works independently of route enforcement):
247
247
  * ```tsx
248
248
  * import { NavigationItem } from '@jmruthers/pace-core';
249
249
  *
@@ -261,13 +261,15 @@ export interface PaceAppLayoutProps {
261
261
  * <PaceAppLayout
262
262
  * appName="My Custom App"
263
263
  * navItems={customNavItems}
264
- * enforcePermissions={true}
264
+ * // Navigation filtering works independently - no need for enforcePermissions
265
265
  * filterNavigationByPermissions={true}
266
266
  * routePermissions={{
267
267
  * '/components': 'read',
268
268
  * '/styles': 'read',
269
269
  * '/meals': 'read'
270
270
  * }}
271
+ * // Optionally enable route-level enforcement (separate from navigation filtering)
272
+ * // enforcePermissions={true}
271
273
  * />
272
274
  * }>
273
275
  * <Route path="components" element={<ComponentsPage />} />
@@ -367,12 +369,24 @@ export function PaceAppLayout({
367
369
 
368
370
  // Check if user is super admin first - super admins can access everything
369
371
  // regardless of organisation context
370
- const { isSuperAdmin } = await import('../../rbac/api');
371
- const isSuper = await isSuperAdmin(user.id);
372
-
373
- if (isSuper) {
374
- // Super admin bypass - allow access regardless of organisation context
375
- return true;
372
+ // Gracefully handle RBAC not being initialized (e.g., in tests)
373
+ try {
374
+ const { isSuperAdmin } = await import('../../rbac/api');
375
+ const isSuper = await isSuperAdmin(user.id);
376
+
377
+ if (isSuper) {
378
+ // Super admin bypass - allow access regardless of organisation context
379
+ return true;
380
+ }
381
+ } catch (error) {
382
+ // If RBAC is not initialized (e.g., in tests), continue with normal permission check
383
+ // This prevents errors from breaking permission checks when RBAC isn't available
384
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'RBAC_NOT_INITIALIZED') {
385
+ // RBAC not available - proceed with normal permission check
386
+ } else {
387
+ // Re-throw unexpected errors
388
+ throw error;
389
+ }
376
390
  }
377
391
 
378
392
  // For non-super admins, ensure we have at least organisationId for RBAC
@@ -498,10 +512,12 @@ export function PaceAppLayout({
498
512
  }, [enforcePermissions, currentRoutePermission, currentPageId, strictMode, user?.id]);
499
513
 
500
514
  // Filter navigation items based on permissions
515
+ // This works independently of route enforcement - navigation filtering doesn't require enforcePermissions
501
516
  const [filteredMenuItems, setFilteredMenuItems] = useState<NavigationItem[]>(baseMenuItems);
502
517
 
503
518
  useEffect(() => {
504
- if (!filterNavigationByPermissions || !enforcePermissions) {
519
+ // Allow navigation filtering without route enforcement
520
+ if (!filterNavigationByPermissions) {
505
521
  setFilteredMenuItems(baseMenuItems);
506
522
  return;
507
523
  }
@@ -509,6 +525,58 @@ export function PaceAppLayout({
509
525
  let isMounted = true;
510
526
 
511
527
  const filterItems = async () => {
528
+ // Wait for organisation context to be ready before filtering
529
+ // This prevents blocking navigation while context is loading
530
+ if (!user?.id) {
531
+ // User not loaded yet - show all items until context is ready
532
+ if (isMounted) {
533
+ setFilteredMenuItems(baseMenuItems);
534
+ }
535
+ return;
536
+ }
537
+
538
+ // Check if organisation context is available
539
+ const scope = {
540
+ organisationId: user.user_metadata?.organisationId || user.app_metadata?.organisationId,
541
+ eventId: user.user_metadata?.eventId || user.app_metadata?.eventId,
542
+ appId: user.user_metadata?.appId || user.app_metadata?.appId,
543
+ };
544
+
545
+ // For super admins, show all items (they bypass permission checks)
546
+ // Gracefully handle RBAC not being initialized (e.g., in tests)
547
+ try {
548
+ const { isSuperAdmin } = await import('../../rbac/api');
549
+ const isSuper = await isSuperAdmin(user.id);
550
+
551
+ if (isSuper) {
552
+ // Super admins see all navigation items
553
+ if (isMounted) {
554
+ setFilteredMenuItems(baseMenuItems);
555
+ }
556
+ return;
557
+ }
558
+ } catch (error) {
559
+ // If RBAC is not initialized (e.g., in tests), continue with normal filtering
560
+ // This prevents errors from breaking navigation when RBAC isn't available
561
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'RBAC_NOT_INITIALIZED') {
562
+ // RBAC not available - proceed with normal filtering without super admin check
563
+ // In this case, we'll filter items normally based on permissions
564
+ } else {
565
+ // Re-throw unexpected errors
566
+ throw error;
567
+ }
568
+ }
569
+
570
+ // If no organisation context yet, show all items until context is ready
571
+ // This prevents navigation from being empty while context loads
572
+ if (!scope.organisationId) {
573
+ if (isMounted) {
574
+ setFilteredMenuItems(baseMenuItems);
575
+ }
576
+ return;
577
+ }
578
+
579
+ // Organisation context is ready - now filter items based on permissions
512
580
  const filtered = await Promise.all(
513
581
  baseMenuItems.map(async (item) => {
514
582
  if (!item.href) return { item, hasAccess: true };
@@ -520,6 +588,7 @@ export function PaceAppLayout({
520
588
  const hasAccess = await checkPermission(permission, pageId);
521
589
  return { item, hasAccess };
522
590
  } catch {
591
+ // On error, default to hiding the item (fail-safe)
523
592
  return { item, hasAccess: false };
524
593
  }
525
594
  })
@@ -539,7 +608,7 @@ export function PaceAppLayout({
539
608
  return () => {
540
609
  isMounted = false;
541
610
  };
542
- }, [baseMenuItems, filterNavigationByPermissions, enforcePermissions, pageIdMapping, routePermissions, defaultPermission]);
611
+ }, [baseMenuItems, filterNavigationByPermissions, pageIdMapping, routePermissions, defaultPermission, checkPermission, user?.id, user?.user_metadata, user?.app_metadata]);
543
612
 
544
613
  // NEW: Phase 2 - Enhanced Routing Features
545
614
  // Check route access for role-based routing
@@ -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