@jmruthers/pace-core 0.5.107 → 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.
- package/dist/{DataTable-H2WIR2DN.js → DataTable-WFCHVWTY.js} +2 -2
- package/dist/{PublicLoadingSpinner-48ewSMKK.d.ts → PublicLoadingSpinner-DgDWTFqn.d.ts} +4 -2
- package/dist/{chunk-EWKCROSF.js → chunk-B3QX32P5.js} +47 -8
- package/dist/chunk-B3QX32P5.js.map +1 -0
- package/dist/{chunk-5JJCXTVE.js → chunk-IMZGJ2X7.js} +105 -83
- package/dist/{chunk-5JJCXTVE.js.map → chunk-IMZGJ2X7.js.map} +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.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/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.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 +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.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/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.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/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.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/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.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/SwitchProps.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/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.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 +7 -5
- package/package.json +1 -1
- package/src/components/DataTable/components/ColumnFilter.tsx +2 -1
- package/src/components/DataTable/components/EditableRow.tsx +7 -2
- package/src/components/DataTable/components/FilterRow.tsx +22 -11
- package/src/components/DataTable/components/PaginationControls.tsx +1 -1
- package/src/components/DataTable/components/UnifiedTableBody.tsx +39 -10
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +79 -10
- package/dist/chunk-EWKCROSF.js.map +0 -1
- /package/dist/{DataTable-H2WIR2DN.js.map → DataTable-WFCHVWTY.js.map} +0 -0
package/docs/api/modules.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
[@jmruthers/pace-core - v0.5.
|
|
1
|
+
[@jmruthers/pace-core - v0.5.108](README.md) / Exports
|
|
2
2
|
|
|
3
|
-
# @jmruthers/pace-core - v0.5.
|
|
3
|
+
# @jmruthers/pace-core - v0.5.108
|
|
4
4
|
|
|
5
5
|
**`File`**
|
|
6
6
|
|
|
@@ -2355,7 +2355,7 @@ function App() {
|
|
|
2355
2355
|
|
|
2356
2356
|
**`Example`**
|
|
2357
2357
|
|
|
2358
|
-
Custom navigation items with permission filtering:
|
|
2358
|
+
Custom navigation items with permission filtering (works independently of route enforcement):
|
|
2359
2359
|
```tsx
|
|
2360
2360
|
import { NavigationItem } from '@jmruthers/pace-core';
|
|
2361
2361
|
|
|
@@ -2373,13 +2373,15 @@ function App() {
|
|
|
2373
2373
|
<PaceAppLayout
|
|
2374
2374
|
appName="My Custom App"
|
|
2375
2375
|
navItems={customNavItems}
|
|
2376
|
-
enforcePermissions
|
|
2376
|
+
// Navigation filtering works independently - no need for enforcePermissions
|
|
2377
2377
|
filterNavigationByPermissions={true}
|
|
2378
2378
|
routePermissions={{
|
|
2379
2379
|
'/components': 'read',
|
|
2380
2380
|
'/styles': 'read',
|
|
2381
2381
|
'/meals': 'read'
|
|
2382
2382
|
}}
|
|
2383
|
+
// Optionally enable route-level enforcement (separate from navigation filtering)
|
|
2384
|
+
// enforcePermissions={true}
|
|
2383
2385
|
/>
|
|
2384
2386
|
}>
|
|
2385
2387
|
<Route path="components" element={<ComponentsPage />} />
|
|
@@ -2433,7 +2435,7 @@ function AdminApp() {
|
|
|
2433
2435
|
|
|
2434
2436
|
#### Defined in
|
|
2435
2437
|
|
|
2436
|
-
[packages/core/src/components/PaceAppLayout/PaceAppLayout.tsx:
|
|
2438
|
+
[packages/core/src/components/PaceAppLayout/PaceAppLayout.tsx:324](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/components/PaceAppLayout/PaceAppLayout.tsx#L324)
|
|
2437
2439
|
|
|
2438
2440
|
___
|
|
2439
2441
|
|
package/package.json
CHANGED
|
@@ -62,13 +62,14 @@ export function ColumnFilter({
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
case 'number':
|
|
65
|
+
// Always hide spinner arrows for number filter inputs (cleaner UX)
|
|
65
66
|
return (
|
|
66
67
|
<Input
|
|
67
68
|
type="number"
|
|
68
69
|
value={columnFilterValue as string || ''}
|
|
69
70
|
onChange={(e) => handleFilterChange(e.target.value ? Number(e.target.value) : undefined)}
|
|
70
71
|
placeholder={placeholder || `Filter ${column.id}...`}
|
|
71
|
-
className="h-8"
|
|
72
|
+
className="h-8 datatable-number-no-spinners"
|
|
72
73
|
/>
|
|
73
74
|
);
|
|
74
75
|
|
|
@@ -44,7 +44,10 @@ function SelectEditField<TData extends DataRecord>({
|
|
|
44
44
|
onChange: (value: CellValue) => void;
|
|
45
45
|
className?: string;
|
|
46
46
|
}) {
|
|
47
|
-
|
|
47
|
+
// Determine if searchable - explicitly check for true to ensure visible search input appears
|
|
48
|
+
// When selectSearchable is true or undefined, show the visible search input box
|
|
49
|
+
// When selectSearchable is false, hide the search input (type-to-search still works via SelectContent internals)
|
|
50
|
+
const isSearchable = columnDef.selectSearchable !== false;
|
|
48
51
|
const isCreatable = columnDef.creatable === true;
|
|
49
52
|
const selectRef = React.useRef<HTMLFormElement>(null);
|
|
50
53
|
const [searchTerm, setSearchTerm] = React.useState('');
|
|
@@ -137,7 +140,7 @@ function SelectEditField<TData extends DataRecord>({
|
|
|
137
140
|
<SelectValue placeholder={placeholder || `Select ${columnDef.header || 'option'}...`} />
|
|
138
141
|
</SelectTrigger>
|
|
139
142
|
<SelectContent
|
|
140
|
-
searchable={isSearchable}
|
|
143
|
+
searchable={Boolean(isSearchable)}
|
|
141
144
|
searchPlaceholder={`Search ${columnDef.header || 'options'}...`}
|
|
142
145
|
maxHeight={columnDef.selectMaxHeight}
|
|
143
146
|
className={columnDef.selectContentClassName}
|
|
@@ -232,6 +235,8 @@ const renderEditField = <TData extends DataRecord>(
|
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
if (columnDef.fieldType === 'number') {
|
|
238
|
+
// Hide spinner arrows by default for number, currency, and percentage fields
|
|
239
|
+
// Only show spinners if explicitly set to false
|
|
235
240
|
const hideSpinners = columnDef.hideNumberSpinners !== false; // Default to true
|
|
236
241
|
return (
|
|
237
242
|
<Input
|
|
@@ -12,7 +12,7 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
|
|
|
12
12
|
const { columnFilters } = getState();
|
|
13
13
|
|
|
14
14
|
// Get unique values for select filters
|
|
15
|
-
const getColumnOptions = (columnId: string) => {
|
|
15
|
+
const getColumnOptions = React.useCallback((columnId: string) => {
|
|
16
16
|
const column = table.getColumn(columnId);
|
|
17
17
|
if (!column) return [];
|
|
18
18
|
|
|
@@ -40,44 +40,55 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
|
|
|
40
40
|
return Array.from(uniqueValues)
|
|
41
41
|
.sort()
|
|
42
42
|
.map((value) => ({ value, label: value }));
|
|
43
|
-
};
|
|
43
|
+
}, [table]);
|
|
44
44
|
|
|
45
45
|
// Determine filter type based on column data
|
|
46
|
-
|
|
46
|
+
// IMPORTANT: Explicit filterType always takes priority - auto-detection only runs if filterType is not set
|
|
47
|
+
const getFilterType = React.useCallback((columnId: string) => {
|
|
47
48
|
const column = table.getColumn(columnId);
|
|
48
49
|
if (!column) return 'text';
|
|
49
50
|
|
|
50
51
|
const columnDef = column.columnDef as any;
|
|
51
52
|
|
|
52
|
-
// Check if column has explicit filter type configuration
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// PRIORITY 1: Check if column has explicit filter type configuration
|
|
54
|
+
// This MUST be checked first and must respect any explicit value, including 'text'
|
|
55
|
+
// Use explicit !== undefined && !== null check to ensure 'text' is not treated as falsy
|
|
56
|
+
// This prevents auto-detection from overriding explicit filterType settings
|
|
57
|
+
const explicitFilterType = columnDef.filterType;
|
|
58
|
+
if (explicitFilterType !== undefined && explicitFilterType !== null && explicitFilterType !== '') {
|
|
59
|
+
// Explicit filterType set - return it immediately (no auto-detection)
|
|
60
|
+
// This ensures filterType: 'text' is always respected, even for columns with ≤10 unique values
|
|
61
|
+
return explicitFilterType as 'text' | 'select' | 'number' | 'date';
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
//
|
|
64
|
+
// Only proceed with auto-detection if filterType was NOT explicitly set
|
|
65
|
+
|
|
66
|
+
// PRIORITY 2: Auto-detect select filter if filterSelectOptions is explicitly provided
|
|
58
67
|
if (columnDef.filterSelectOptions && Array.isArray(columnDef.filterSelectOptions)) {
|
|
59
68
|
return 'select';
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
// Check if it's a date column
|
|
71
|
+
// PRIORITY 3: Check if it's a date column (by column ID pattern)
|
|
63
72
|
if (columnId.toLowerCase().includes('date') || columnId.toLowerCase().includes('time')) {
|
|
64
73
|
return 'date';
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
// Check if it's a number column
|
|
76
|
+
// PRIORITY 4: Check if it's a number column (by data type)
|
|
68
77
|
const firstValue = table.getRowModel().rows[0]?.getValue(columnId);
|
|
69
78
|
if (typeof firstValue === 'number') {
|
|
70
79
|
return 'number';
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
//
|
|
82
|
+
// PRIORITY 5: Auto-detect select filter if limited unique values (≤10)
|
|
83
|
+
// Only runs if filterType was NOT explicitly set (checked above)
|
|
74
84
|
const uniqueValues = getColumnOptions(columnId);
|
|
75
85
|
if (uniqueValues.length <= 10 && uniqueValues.length > 1) {
|
|
76
86
|
return 'select';
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
// Default to text filter
|
|
79
90
|
return 'text';
|
|
80
|
-
};
|
|
91
|
+
}, [table, getColumnOptions]);
|
|
81
92
|
|
|
82
93
|
return (
|
|
83
94
|
<tr className="border-b bg-sec-50/50">
|
|
@@ -259,7 +259,7 @@ export function EnhancedPaginationControls<TData extends DataRecord>({
|
|
|
259
259
|
max={pageCount}
|
|
260
260
|
value={jumpToPage}
|
|
261
261
|
onChange={(e) => setJumpToPage(e.target.value)}
|
|
262
|
-
className="w-16 h-6 px-2 border rounded text-xs"
|
|
262
|
+
className="w-16 h-6 px-2 border rounded text-xs datatable-number-no-spinners"
|
|
263
263
|
placeholder="1"
|
|
264
264
|
/>
|
|
265
265
|
<Button type="submit" size="sm" variant="outline" className="h-6 px-2 text-xs">
|
|
@@ -134,7 +134,10 @@ function SelectEditField<TData extends DataRecord>({
|
|
|
134
134
|
placeholder?: string;
|
|
135
135
|
onChange: (value: CellValue) => void;
|
|
136
136
|
}) {
|
|
137
|
-
|
|
137
|
+
// Determine if searchable - explicitly check for true to ensure visible search input appears
|
|
138
|
+
// When selectSearchable is true or undefined, show the visible search input box
|
|
139
|
+
// When selectSearchable is false, hide the search input (type-to-search still works via SelectContent internals)
|
|
140
|
+
const isSearchable = columnDef.selectSearchable !== false;
|
|
138
141
|
const isCreatable = columnDef.creatable === true;
|
|
139
142
|
const selectRef = React.useRef<HTMLFormElement>(null);
|
|
140
143
|
const [searchTerm, setSearchTerm] = React.useState('');
|
|
@@ -227,7 +230,7 @@ function SelectEditField<TData extends DataRecord>({
|
|
|
227
230
|
<SelectValue placeholder={placeholder || `Select ${columnDef.header || 'option'}...`} />
|
|
228
231
|
</SelectTrigger>
|
|
229
232
|
<SelectContent
|
|
230
|
-
searchable={isSearchable}
|
|
233
|
+
searchable={Boolean(isSearchable)}
|
|
231
234
|
searchPlaceholder={`Search ${columnDef.header || 'options'}...`}
|
|
232
235
|
maxHeight={columnDef.selectMaxHeight}
|
|
233
236
|
className={columnDef.selectContentClassName}
|
|
@@ -312,8 +315,11 @@ const renderEditField = <TData extends DataRecord>(
|
|
|
312
315
|
);
|
|
313
316
|
}
|
|
314
317
|
|
|
315
|
-
// Check for number type
|
|
318
|
+
// Check for number type (applies to number, currency, and percentage fields)
|
|
316
319
|
if (columnDef.fieldType === 'number') {
|
|
320
|
+
// Hide spinner arrows by default for all number-related fields
|
|
321
|
+
// Currency and percentage columns use fieldType: 'number' with formatting in cell renderer
|
|
322
|
+
// Only show spinners if explicitly set to false
|
|
317
323
|
const hideSpinners = columnDef.hideNumberSpinners !== false; // Default to true
|
|
318
324
|
return (
|
|
319
325
|
<Input
|
|
@@ -879,6 +885,26 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
879
885
|
}
|
|
880
886
|
|
|
881
887
|
// Render edit fields for data columns
|
|
888
|
+
// Determine the correct key to use for creationData
|
|
889
|
+
// Priority: editAccessorKey > accessorKey > column.id
|
|
890
|
+
const columnDef = header.column.columnDef as EditableColumnDef<TData>;
|
|
891
|
+
const dataKey = columnDef.editAccessorKey || columnDef.accessorKey || header.column.id;
|
|
892
|
+
|
|
893
|
+
// Always render a cell to maintain alignment - renderEditField always returns something
|
|
894
|
+
const editField = renderEditField(
|
|
895
|
+
header.column,
|
|
896
|
+
creationData[dataKey] ?? creationData[header.column.id] ?? '',
|
|
897
|
+
(value) => {
|
|
898
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
|
|
899
|
+
onCreationDataChange({ ...creationData, ...(value as Record<string, CellValue>) });
|
|
900
|
+
} else {
|
|
901
|
+
// Use the determined dataKey for consistent data access
|
|
902
|
+
onCreationDataChange({ ...creationData, [dataKey]: value as CellValue });
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
creationData
|
|
906
|
+
);
|
|
907
|
+
|
|
882
908
|
return (
|
|
883
909
|
<td
|
|
884
910
|
key={header.column.id}
|
|
@@ -887,13 +913,16 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
887
913
|
className: "px-3 py-2"
|
|
888
914
|
})}
|
|
889
915
|
>
|
|
890
|
-
{
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
916
|
+
{editField || (
|
|
917
|
+
// Fallback: render a text input if renderEditField somehow returns nothing
|
|
918
|
+
<Input
|
|
919
|
+
type="text"
|
|
920
|
+
value={String(creationData[dataKey] ?? creationData[header.column.id] ?? '')}
|
|
921
|
+
onChange={(e) => onCreationDataChange({ ...creationData, [dataKey]: e.target.value as CellValue })}
|
|
922
|
+
placeholder={`Enter ${columnDef.header || header.column.id}...`}
|
|
923
|
+
className="h-8"
|
|
924
|
+
/>
|
|
925
|
+
)}
|
|
897
926
|
</td>
|
|
898
927
|
);
|
|
899
928
|
})}
|
|
@@ -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
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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,
|
|
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
|