@jmruthers/pace-core 0.6.2 → 0.6.4
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/CHANGELOG.md +45 -0
- package/cursor-rules/00-pace-core-compliance.mdc +34 -2
- package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
- package/dist/{DataTable-TPTKCX4D.js → DataTable-E7YQZD7D.js} +9 -8
- package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
- package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
- package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-QPXO24B4.js} +5 -4
- package/dist/{api-MVVQZLJI.js → api-6LVZTHDS.js} +10 -10
- package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
- package/dist/chunk-36LVWXB2.js +227 -0
- package/dist/chunk-36LVWXB2.js.map +1 -0
- package/dist/{chunk-24UVZUZG.js → chunk-3LPHPB62.js} +129 -387
- package/dist/chunk-3LPHPB62.js.map +1 -0
- package/dist/{chunk-2UOI2FG5.js → chunk-5EC5MEWX.js} +4 -4
- package/dist/{chunk-3XC4CPTD.js → chunk-7JPAB3T5.js} +244 -5727
- package/dist/chunk-7JPAB3T5.js.map +1 -0
- package/dist/{chunk-6J4GEEJR.js → chunk-ATKZM7RX.js} +53 -27
- package/dist/chunk-ATKZM7RX.js.map +1 -0
- package/dist/{chunk-EHMR7VYL.js → chunk-AVMLPIM7.js} +443 -189
- package/dist/chunk-AVMLPIM7.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-NECFR5MM.js → chunk-I6DAQMWX.js} +575 -647
- package/dist/chunk-I6DAQMWX.js.map +1 -0
- package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
- package/dist/{chunk-XWQCNGTQ.js → chunk-NN6WWZ5U.js} +173 -79
- package/dist/chunk-NN6WWZ5U.js.map +1 -0
- package/dist/{chunk-MMZ7JXPU.js → chunk-OEWDTMG7.js} +13 -21
- package/dist/{chunk-MMZ7JXPU.js.map → chunk-OEWDTMG7.js.map} +1 -1
- package/dist/{chunk-SFZUDBL5.js → chunk-YKRAFF5K.js} +70 -56
- package/dist/chunk-YKRAFF5K.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +12 -13
- package/dist/contextValidator-OOPCLPZW.js +9 -0
- package/dist/contextValidator-OOPCLPZW.js.map +1 -0
- package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +21 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +4 -3
- package/dist/rbac/index.d.ts +67 -27
- package/dist/rbac/index.js +15 -8
- package/dist/styles/index.js +1 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
- package/dist/utils.js +5 -7
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +14 -16
- package/docs/api/modules.md +3796 -2513
- package/docs/components/context-selector.md +126 -0
- package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
- package/docs/pace-mint-fix-auto-selection.md +218 -0
- package/docs/pace-mint-rbac-setup.md +391 -0
- package/docs/rbac/secure-client-protection.md +330 -0
- package/package.json +10 -5
- package/scripts/audit/core/checks/compliance.cjs +72 -0
- package/scripts/audit/core/checks/dependencies.cjs +568 -28
- package/scripts/audit/core/checks/documentation.cjs +68 -3
- package/scripts/audit/core/checks/environment.cjs +2 -14
- package/scripts/audit/core/checks/error-handling.cjs +47 -6
- package/src/components/ContextSelector/ContextSelector.tsx +384 -0
- package/src/components/ContextSelector/index.ts +3 -0
- package/src/components/DataTable/components/RowComponent.tsx +19 -19
- package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
- package/src/components/Dialog/Dialog.tsx +29 -1
- package/src/components/FileDisplay/FileDisplay.tsx +42 -10
- package/src/components/Header/Header.test.tsx +43 -73
- package/src/components/Header/Header.tsx +44 -45
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +157 -36
- package/src/components/PaceAppLayout/README.md +14 -17
- package/src/components/PaceAppLayout/test-setup.tsx +2 -2
- package/src/components/index.ts +5 -5
- package/src/eslint-rules/pace-core-compliance.cjs +106 -0
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
- package/src/hooks/useAppConfig.ts +15 -30
- package/src/hooks/useFileDisplay.ts +77 -50
- package/src/index.ts +4 -5
- package/src/providers/services/AuthServiceProvider.tsx +17 -7
- package/src/providers/services/EventServiceProvider.tsx +33 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
- package/src/rbac/adapters.tsx +2 -2
- package/src/rbac/api.test.ts +59 -51
- package/src/rbac/api.ts +178 -132
- package/src/rbac/components/PagePermissionGuard.tsx +38 -10
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
- package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
- package/src/rbac/hooks/permissions/useCan.ts +41 -11
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +0 -9
- package/src/rbac/hooks/useRBAC.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.ts +36 -37
- package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
- package/src/rbac/hooks/useResolvedScope.ts +35 -40
- package/src/rbac/hooks/useSecureSupabase.ts +7 -7
- package/src/rbac/index.ts +7 -0
- package/src/rbac/secureClient.test.ts +22 -18
- package/src/rbac/secureClient.ts +103 -16
- package/src/rbac/security.ts +0 -17
- package/src/rbac/types.ts +1 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
- package/src/rbac/utils/clientSecurity.ts +93 -0
- package/src/rbac/utils/contextValidator.ts +77 -168
- package/src/services/AuthService.ts +39 -7
- package/src/services/EventService.ts +285 -56
- package/src/services/OrganisationService.ts +81 -14
- package/src/services/__tests__/EventService.test.ts +1 -2
- package/src/services/base/BaseService.ts +3 -0
- package/src/utils/dynamic/dynamicUtils.ts +7 -4
- package/dist/chunk-24UVZUZG.js.map +0 -1
- package/dist/chunk-3XC4CPTD.js.map +0 -1
- package/dist/chunk-6J4GEEJR.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-EHMR7VYL.js.map +0 -1
- package/dist/chunk-NECFR5MM.js.map +0 -1
- package/dist/chunk-SFZUDBL5.js.map +0 -1
- package/dist/chunk-XWQCNGTQ.js.map +0 -1
- package/docs/api/classes/ColumnFactory.md +0 -243
- package/docs/api/classes/InvalidScopeError.md +0 -73
- package/docs/api/classes/Logger.md +0 -178
- package/docs/api/classes/MissingUserContextError.md +0 -66
- package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
- package/docs/api/classes/PermissionDeniedError.md +0 -73
- package/docs/api/classes/RBACAuditManager.md +0 -297
- package/docs/api/classes/RBACCache.md +0 -322
- package/docs/api/classes/RBACEngine.md +0 -171
- package/docs/api/classes/RBACError.md +0 -76
- package/docs/api/classes/RBACNotInitializedError.md +0 -66
- package/docs/api/classes/SecureSupabaseClient.md +0 -163
- package/docs/api/classes/StorageUtils.md +0 -328
- package/docs/api/enums/FileCategory.md +0 -184
- package/docs/api/enums/LogLevel.md +0 -54
- package/docs/api/enums/RBACErrorCode.md +0 -228
- package/docs/api/enums/RPCFunction.md +0 -118
- package/docs/api/interfaces/AddressFieldProps.md +0 -241
- package/docs/api/interfaces/AddressFieldRef.md +0 -94
- package/docs/api/interfaces/AggregateConfig.md +0 -43
- package/docs/api/interfaces/AutocompleteOptions.md +0 -75
- package/docs/api/interfaces/AvatarProps.md +0 -128
- package/docs/api/interfaces/BadgeProps.md +0 -34
- package/docs/api/interfaces/ButtonProps.md +0 -56
- package/docs/api/interfaces/CalendarProps.md +0 -73
- package/docs/api/interfaces/CardProps.md +0 -69
- package/docs/api/interfaces/ColorPalette.md +0 -7
- package/docs/api/interfaces/ColorShade.md +0 -66
- package/docs/api/interfaces/ComplianceResult.md +0 -30
- package/docs/api/interfaces/DataAccessRecord.md +0 -96
- package/docs/api/interfaces/DataRecord.md +0 -11
- package/docs/api/interfaces/DataTableAction.md +0 -252
- package/docs/api/interfaces/DataTableColumn.md +0 -504
- package/docs/api/interfaces/DataTableProps.md +0 -625
- package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
- package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
- package/docs/api/interfaces/DatabaseIssue.md +0 -41
- package/docs/api/interfaces/EmptyStateConfig.md +0 -61
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
- package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
- package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
- package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
- package/docs/api/interfaces/EventAppRoleData.md +0 -71
- package/docs/api/interfaces/ExportColumn.md +0 -90
- package/docs/api/interfaces/ExportOptions.md +0 -126
- package/docs/api/interfaces/FileDisplayProps.md +0 -249
- package/docs/api/interfaces/FileMetadata.md +0 -129
- package/docs/api/interfaces/FileReference.md +0 -118
- package/docs/api/interfaces/FileSizeLimits.md +0 -7
- package/docs/api/interfaces/FileUploadOptions.md +0 -139
- package/docs/api/interfaces/FileUploadProps.md +0 -296
- package/docs/api/interfaces/FooterProps.md +0 -107
- package/docs/api/interfaces/FormFieldProps.md +0 -166
- package/docs/api/interfaces/FormProps.md +0 -113
- package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
- package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
- package/docs/api/interfaces/InputProps.md +0 -56
- package/docs/api/interfaces/LabelProps.md +0 -107
- package/docs/api/interfaces/LoggerConfig.md +0 -62
- package/docs/api/interfaces/LoginFormProps.md +0 -187
- package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
- package/docs/api/interfaces/NavigationContextType.md +0 -164
- package/docs/api/interfaces/NavigationGuardProps.md +0 -139
- package/docs/api/interfaces/NavigationItem.md +0 -120
- package/docs/api/interfaces/NavigationMenuProps.md +0 -221
- package/docs/api/interfaces/NavigationProviderProps.md +0 -117
- package/docs/api/interfaces/Organisation.md +0 -140
- package/docs/api/interfaces/OrganisationContextType.md +0 -388
- package/docs/api/interfaces/OrganisationMembership.md +0 -140
- package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
- package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
- package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
- package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
- package/docs/api/interfaces/PageAccessRecord.md +0 -85
- package/docs/api/interfaces/PagePermissionContextType.md +0 -140
- package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
- package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
- package/docs/api/interfaces/PaletteData.md +0 -41
- package/docs/api/interfaces/ParsedAddress.md +0 -120
- package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
- package/docs/api/interfaces/ProgressProps.md +0 -42
- package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
- package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
- package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
- package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
- package/docs/api/interfaces/QuickFix.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
- package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
- package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
- package/docs/api/interfaces/RBACConfig.md +0 -133
- package/docs/api/interfaces/RBACContext.md +0 -52
- package/docs/api/interfaces/RBACLogger.md +0 -112
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
- package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
- package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
- package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
- package/docs/api/interfaces/RBACResult.md +0 -58
- package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
- package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
- package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
- package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
- package/docs/api/interfaces/RBACRolesListParams.md +0 -52
- package/docs/api/interfaces/RBACRolesListResult.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
- package/docs/api/interfaces/ResourcePermissions.md +0 -155
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
- package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
- package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
- package/docs/api/interfaces/RoleManagementResult.md +0 -52
- package/docs/api/interfaces/RouteAccessRecord.md +0 -107
- package/docs/api/interfaces/RouteConfig.md +0 -134
- package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
- package/docs/api/interfaces/SecureDataContextType.md +0 -168
- package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
- package/docs/api/interfaces/SetupIssue.md +0 -41
- package/docs/api/interfaces/StorageConfig.md +0 -41
- package/docs/api/interfaces/StorageFileInfo.md +0 -74
- package/docs/api/interfaces/StorageFileMetadata.md +0 -151
- package/docs/api/interfaces/StorageListOptions.md +0 -99
- package/docs/api/interfaces/StorageListResult.md +0 -41
- package/docs/api/interfaces/StorageUploadOptions.md +0 -101
- package/docs/api/interfaces/StorageUploadResult.md +0 -63
- package/docs/api/interfaces/StorageUrlOptions.md +0 -60
- package/docs/api/interfaces/StyleImport.md +0 -19
- package/docs/api/interfaces/SwitchProps.md +0 -34
- package/docs/api/interfaces/TabsContentProps.md +0 -9
- package/docs/api/interfaces/TabsListProps.md +0 -9
- package/docs/api/interfaces/TabsProps.md +0 -9
- package/docs/api/interfaces/TabsTriggerProps.md +0 -50
- package/docs/api/interfaces/TextareaProps.md +0 -53
- package/docs/api/interfaces/ToastActionElement.md +0 -12
- package/docs/api/interfaces/ToastProps.md +0 -9
- package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
- package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
- package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
- package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
- package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
- package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
- package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
- package/docs/api/interfaces/UserEventAccess.md +0 -121
- package/docs/api/interfaces/UserMenuProps.md +0 -88
- package/docs/api/interfaces/UserProfile.md +0 -63
- package/src/components/EventSelector/EventSelector.test.tsx +0 -720
- package/src/components/EventSelector/EventSelector.tsx +0 -423
- package/src/components/EventSelector/index.ts +0 -3
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
- package/src/components/OrganisationSelector/index.ts +0 -9
- /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-E7YQZD7D.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-QPXO24B4.js.map} +0 -0
- /package/dist/{api-MVVQZLJI.js.map → api-6LVZTHDS.js.map} +0 -0
- /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
- /package/dist/{chunk-2UOI2FG5.js.map → chunk-5EC5MEWX.js.map} +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
|
@@ -97,6 +97,14 @@ const documentationCheck = {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// Find exported functions/components
|
|
100
|
+
// Match various export patterns:
|
|
101
|
+
// - export function Name
|
|
102
|
+
// - export const Name = ...
|
|
103
|
+
// - export class Name
|
|
104
|
+
// - export interface Name
|
|
105
|
+
// - export type Name
|
|
106
|
+
// - export default function Name
|
|
107
|
+
// - export default const Name = ...
|
|
100
108
|
const exportPattern = /export\s+(?:default\s+)?(?:async\s+)?(function|const|class|interface|type)\s+(\w+)/g;
|
|
101
109
|
let match;
|
|
102
110
|
while ((match = exportPattern.exec(content)) !== null) {
|
|
@@ -105,9 +113,66 @@ const documentationCheck = {
|
|
|
105
113
|
const exportIndex = match.index;
|
|
106
114
|
|
|
107
115
|
// Check if there's JSDoc before this export
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
// Search backwards up to 2000 characters, but stop at:
|
|
117
|
+
// - Previous export statement
|
|
118
|
+
// - Previous function/class/interface/type declaration (non-exported)
|
|
119
|
+
// - File start
|
|
120
|
+
const searchStart = Math.max(0, exportIndex - 2000);
|
|
121
|
+
const beforeExport = content.substring(searchStart, exportIndex);
|
|
122
|
+
|
|
123
|
+
// Find the last export/declaration before this one to avoid false positives
|
|
124
|
+
// from JSDoc comments that belong to previous exports
|
|
125
|
+
const previousExportMatch = beforeExport.match(/export\s+(?:default\s+)?(?:async\s+)?(function|const|class|interface|type)\s+\w+/);
|
|
126
|
+
const previousDeclMatch = beforeExport.match(/(?:^|\n)\s*(?:async\s+)?(function|const|class|interface|type)\s+\w+/);
|
|
127
|
+
|
|
128
|
+
let searchWindow = beforeExport;
|
|
129
|
+
if (previousExportMatch && previousExportMatch.index !== undefined) {
|
|
130
|
+
// Start search after the previous export
|
|
131
|
+
const prevExportEnd = previousExportMatch.index + previousExportMatch[0].length;
|
|
132
|
+
searchWindow = beforeExport.substring(prevExportEnd);
|
|
133
|
+
} else if (previousDeclMatch && previousDeclMatch.index !== undefined) {
|
|
134
|
+
// Start search after the previous declaration (but be more lenient)
|
|
135
|
+
const prevDeclEnd = previousDeclMatch.index + previousDeclMatch[0].length;
|
|
136
|
+
// Only use this if it's close to our export (within 500 chars)
|
|
137
|
+
if (prevDeclEnd > beforeExport.length - 500) {
|
|
138
|
+
searchWindow = beforeExport.substring(prevDeclEnd);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for JSDoc in the search window
|
|
143
|
+
// Look for standard JSDoc block comments /** ... */
|
|
144
|
+
// Also check for TypeScript triple-slash reference comments /// <
|
|
145
|
+
// Check if JSDoc ends close to our export (within last 100 chars of search window)
|
|
146
|
+
const jsdocBlockMatch = searchWindow.match(/\/\*\*[\s\S]*?\*\//g);
|
|
147
|
+
const jsdocRefMatch = searchWindow.match(/\/\/\/\s*</g);
|
|
148
|
+
|
|
149
|
+
let hasJSDoc = false;
|
|
150
|
+
|
|
151
|
+
// Check if JSDoc block comment ends close to our export
|
|
152
|
+
if (jsdocBlockMatch && jsdocBlockMatch.length > 0) {
|
|
153
|
+
const lastJSDoc = jsdocBlockMatch[jsdocBlockMatch.length - 1];
|
|
154
|
+
const lastJSDocEnd = searchWindow.lastIndexOf(lastJSDoc) + lastJSDoc.length;
|
|
155
|
+
// JSDoc is considered associated if it ends within 200 characters of the export
|
|
156
|
+
// or if there's only whitespace/comments between JSDoc and export
|
|
157
|
+
const distanceToExport = searchWindow.length - lastJSDocEnd;
|
|
158
|
+
const betweenJSDocAndExport = searchWindow.substring(lastJSDocEnd);
|
|
159
|
+
// Check if there's only whitespace, comments, or newlines between JSDoc and export
|
|
160
|
+
// Remove all whitespace and single-line comments, then check if anything remains
|
|
161
|
+
const cleanedBetween = betweenJSDocAndExport
|
|
162
|
+
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
|
163
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments (shouldn't be here, but just in case)
|
|
164
|
+
.replace(/[\s\n\r]/g, ''); // Remove all whitespace
|
|
165
|
+
const onlyWhitespaceBetween = cleanedBetween.length === 0;
|
|
166
|
+
hasJSDoc = distanceToExport <= 200 || onlyWhitespaceBetween;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Also check for TypeScript reference comments
|
|
170
|
+
if (!hasJSDoc && jsdocRefMatch && jsdocRefMatch.length > 0) {
|
|
171
|
+
const lastRef = jsdocRefMatch[jsdocRefMatch.length - 1];
|
|
172
|
+
const lastRefEnd = searchWindow.lastIndexOf(lastRef) + lastRef.length;
|
|
173
|
+
const distanceToExport = searchWindow.length - lastRefEnd;
|
|
174
|
+
hasJSDoc = distanceToExport <= 200;
|
|
175
|
+
}
|
|
111
176
|
|
|
112
177
|
// Only check public exports (not internal utilities)
|
|
113
178
|
const isPublic = match[0].includes('export') &&
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - Missing required environment variables
|
|
10
10
|
* - Hardcoded secrets/API keys
|
|
11
11
|
* - Incorrect environment variable usage
|
|
12
|
-
* - Missing .env.example file
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
const fs = require('fs');
|
|
@@ -17,7 +16,7 @@ const path = require('path');
|
|
|
17
16
|
|
|
18
17
|
const environmentCheck = {
|
|
19
18
|
name: 'environment',
|
|
20
|
-
description: 'Environment variable usage (secrets, required vars
|
|
19
|
+
description: 'Environment variable usage (secrets, required vars)',
|
|
21
20
|
severity: 'warning',
|
|
22
21
|
|
|
23
22
|
async run(context) {
|
|
@@ -26,24 +25,13 @@ const environmentCheck = {
|
|
|
26
25
|
const warnings = [];
|
|
27
26
|
const suggestions = [];
|
|
28
27
|
|
|
29
|
-
// Check for .env.example
|
|
30
|
-
const envExamplePath = path.join(projectRoot, '.env.example');
|
|
31
|
-
if (!fs.existsSync(envExamplePath)) {
|
|
32
|
-
suggestions.push({
|
|
33
|
-
type: 'missing-env-example',
|
|
34
|
-
file: '.env.example',
|
|
35
|
-
message: '.env.example file is missing',
|
|
36
|
-
recommendation: 'Create .env.example with all required environment variables (without actual values)'
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
28
|
// Required environment variables for pace-core apps
|
|
41
29
|
const requiredEnvVars = [
|
|
42
30
|
'VITE_SUPABASE_URL',
|
|
43
31
|
'VITE_SUPABASE_PUBLISHABLE_KEY'
|
|
44
32
|
];
|
|
45
33
|
|
|
46
|
-
// Check .env.example for required vars
|
|
34
|
+
// Check .env.example for required vars (if file exists)
|
|
47
35
|
if (fs.existsSync(envExamplePath)) {
|
|
48
36
|
try {
|
|
49
37
|
const envExampleContent = fs.readFileSync(envExamplePath, 'utf8');
|
|
@@ -37,10 +37,56 @@ const errorHandlingCheck = {
|
|
|
37
37
|
const packagesCorePath = path.join(projectRoot, 'packages', 'core');
|
|
38
38
|
const isPaceCoreRepository = fs.existsSync(packagesCorePath);
|
|
39
39
|
|
|
40
|
-
// Check for ErrorBoundary usage
|
|
40
|
+
// Check for ErrorBoundary usage in main app file
|
|
41
41
|
let hasErrorBoundary = false;
|
|
42
42
|
const mainFiles = ['main.tsx', 'main.ts', 'App.tsx', 'App.ts', 'index.tsx', 'index.ts'];
|
|
43
43
|
|
|
44
|
+
// First, specifically check the main app file for ErrorBoundary usage
|
|
45
|
+
// This must be done before the general file loop to ensure we check the actual main file
|
|
46
|
+
// Previously, the check would skip src/ files (thinking they were demo apps) and only
|
|
47
|
+
// check if ErrorBoundary appeared anywhere in the codebase. Now we specifically check
|
|
48
|
+
// the main.tsx file for proper ErrorBoundary import and JSX usage patterns.
|
|
49
|
+
if (!isPaceCoreRepository) {
|
|
50
|
+
const mainFile = mainFiles.find(file => {
|
|
51
|
+
const filePath = path.join(projectRoot, 'src', file);
|
|
52
|
+
return fs.existsSync(filePath);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (mainFile) {
|
|
56
|
+
const mainFilePath = path.join(projectRoot, 'src', mainFile);
|
|
57
|
+
try {
|
|
58
|
+
const mainContent = fs.readFileSync(mainFilePath, 'utf8');
|
|
59
|
+
|
|
60
|
+
// Check for ErrorBoundary import from pace-core
|
|
61
|
+
// Matches: import { ErrorBoundary } from '@jmruthers/pace-core'
|
|
62
|
+
// Matches: import { ..., ErrorBoundary, ... } from '@jmruthers/pace-core'
|
|
63
|
+
// Matches: import { ErrorBoundary } from '@jmruthers/pace-core/components'
|
|
64
|
+
const hasErrorBoundaryImport = /import\s+[^'"]*ErrorBoundary[^'"]*from\s+['"]@jmruthers\/pace-core/.test(mainContent);
|
|
65
|
+
|
|
66
|
+
// Check for ErrorBoundary JSX usage
|
|
67
|
+
// Matches: <ErrorBoundary ...> or <ErrorBoundary>
|
|
68
|
+
const hasErrorBoundaryJSX = /<ErrorBoundary[\s>]/.test(mainContent);
|
|
69
|
+
|
|
70
|
+
// Check if ErrorBoundary wraps the render call or app component
|
|
71
|
+
// Pattern 1: createRoot(...).render(<ErrorBoundary ...)
|
|
72
|
+
// Pattern 2: render(<ErrorBoundary ...)
|
|
73
|
+
// Pattern 3: <ErrorBoundary ...> wrapping App component or createRoot call
|
|
74
|
+
const hasErrorBoundaryInRender = /createRoot\s*\([^)]*\)\s*\.\s*render\s*\(\s*<ErrorBoundary/.test(mainContent) ||
|
|
75
|
+
/\.render\s*\(\s*<ErrorBoundary/.test(mainContent) ||
|
|
76
|
+
/<ErrorBoundary[\s\S]{0,500}?<App[\s>]/.test(mainContent) ||
|
|
77
|
+
/<ErrorBoundary[\s\S]{0,500}?createRoot/.test(mainContent);
|
|
78
|
+
|
|
79
|
+
// ErrorBoundary is present if it's imported AND used in JSX
|
|
80
|
+
// We check both JSX usage and render wrapping to catch all valid patterns
|
|
81
|
+
if (hasErrorBoundaryImport && (hasErrorBoundaryJSX || hasErrorBoundaryInRender)) {
|
|
82
|
+
hasErrorBoundary = true;
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// If we can't read the main file, continue with other checks
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
44
90
|
for (const filePath of files) {
|
|
45
91
|
try {
|
|
46
92
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
@@ -61,11 +107,6 @@ const errorHandlingCheck = {
|
|
|
61
107
|
continue; // Skip script files
|
|
62
108
|
}
|
|
63
109
|
|
|
64
|
-
// Check for ErrorBoundary
|
|
65
|
-
if (content.includes('ErrorBoundary') || content.includes('error-boundary')) {
|
|
66
|
-
hasErrorBoundary = true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
110
|
// Check for unhandled promise rejections
|
|
70
111
|
const asyncFunctionPattern = /(async\s+function|const\s+\w+\s*=\s*async|export\s+async\s+function)/g;
|
|
71
112
|
let asyncMatch;
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Context Selector Component
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/ContextSelector
|
|
5
|
+
* @since 0.6.4
|
|
6
|
+
*
|
|
7
|
+
* A unified intelligent selector component that shows all organisations and events
|
|
8
|
+
* a user can access based on their roles and permissions. This is the single
|
|
9
|
+
* selector for all apps - it intelligently determines what to show based on:
|
|
10
|
+
* - User's organisation roles
|
|
11
|
+
* - User's event-app roles
|
|
12
|
+
* - Super admin status (shows all)
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Intelligent display of accessible organisations and events
|
|
16
|
+
* - Grouped display (Organisations / Events sections)
|
|
17
|
+
* - Visual distinction between orgs and events
|
|
18
|
+
* - Super admin support (shows all orgs and events)
|
|
19
|
+
* - Loading states and error handling
|
|
20
|
+
* - Accessible interface design
|
|
21
|
+
* - Integration with OrganisationProvider and EventService
|
|
22
|
+
* - Automatically shows superset of what user can access
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <ContextSelector
|
|
27
|
+
* onOrganisationSelect={(org) => {
|
|
28
|
+
* switchOrganisation(org.id);
|
|
29
|
+
* }}
|
|
30
|
+
* onEventSelect={(event) => {
|
|
31
|
+
* setSelectedEvent(event);
|
|
32
|
+
* }}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @accessibility
|
|
37
|
+
* - WCAG 2.1 AA compliant
|
|
38
|
+
* - Keyboard navigation support
|
|
39
|
+
* - Screen reader friendly
|
|
40
|
+
* - Focus management
|
|
41
|
+
* - ARIA labels and descriptions
|
|
42
|
+
* - High contrast support
|
|
43
|
+
*
|
|
44
|
+
* @security
|
|
45
|
+
* - Only shows organisations/events user has access to
|
|
46
|
+
* - Super admins see all items
|
|
47
|
+
* - Validates access before selection
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import React, { useMemo } from 'react';
|
|
51
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel, SelectSeparator } from '../Select';
|
|
52
|
+
import { Alert, AlertDescription } from '../Alert/Alert';
|
|
53
|
+
import { Button } from '../Button/Button';
|
|
54
|
+
import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner';
|
|
55
|
+
import { RefreshCw, AlertCircle, Building2, Calendar } from 'lucide-react';
|
|
56
|
+
import { useOrganisations } from '../../hooks/useOrganisations';
|
|
57
|
+
import { useEvents } from '../../hooks/useEvents';
|
|
58
|
+
import { useRBAC } from '../../rbac/hooks/useRBAC';
|
|
59
|
+
import type { Organisation } from '../../types/organisation';
|
|
60
|
+
import type { Event } from '../../types/event';
|
|
61
|
+
import { logger } from '../../utils/core/logger';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Props for the ContextSelector component.
|
|
65
|
+
*/
|
|
66
|
+
export interface ContextSelectorProps {
|
|
67
|
+
/** Placeholder text for the dropdown */
|
|
68
|
+
placeholder?: string;
|
|
69
|
+
/** Additional CSS classes */
|
|
70
|
+
className?: string;
|
|
71
|
+
/** Callback fired when an organisation is selected */
|
|
72
|
+
onOrganisationSelect?: (org: Organisation) => void;
|
|
73
|
+
/** Callback fired when an event is selected */
|
|
74
|
+
onEventSelect?: (event: Event) => void;
|
|
75
|
+
/** Show friendly message when no items available */
|
|
76
|
+
showNoItemsMessage?: boolean;
|
|
77
|
+
/** Show retry button on errors */
|
|
78
|
+
showRetryButton?: boolean;
|
|
79
|
+
/** Compact display mode */
|
|
80
|
+
compact?: boolean;
|
|
81
|
+
/** Disabled state */
|
|
82
|
+
disabled?: boolean;
|
|
83
|
+
/** Show organisations section (default: true) */
|
|
84
|
+
showOrganisations?: boolean;
|
|
85
|
+
/** Show events section (default: true) */
|
|
86
|
+
showEvents?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* ContextSelector component for selecting organisations or events
|
|
91
|
+
*
|
|
92
|
+
* This is the unified intelligent selector that shows all organisations and events
|
|
93
|
+
* a user can access. It automatically determines what to show based on the user's
|
|
94
|
+
* roles and permissions - no need to configure separate org/event selectors.
|
|
95
|
+
*
|
|
96
|
+
* The selector intelligently shows:
|
|
97
|
+
* - All organisations the user has access to (via organisation roles)
|
|
98
|
+
* - All events the user has access to (via event-app roles or organisation membership)
|
|
99
|
+
* - Everything for super admins
|
|
100
|
+
*
|
|
101
|
+
* @component
|
|
102
|
+
* @example
|
|
103
|
+
* <ContextSelector
|
|
104
|
+
* onOrganisationSelect={(org) => switchOrganisation(org)}
|
|
105
|
+
* onEventSelect={(event) => setSelectedEvent(event)}
|
|
106
|
+
* />
|
|
107
|
+
*/
|
|
108
|
+
export function ContextSelector({
|
|
109
|
+
placeholder = "Select organisation or event",
|
|
110
|
+
className,
|
|
111
|
+
onOrganisationSelect,
|
|
112
|
+
onEventSelect,
|
|
113
|
+
showNoItemsMessage = true,
|
|
114
|
+
showRetryButton = true,
|
|
115
|
+
compact = false,
|
|
116
|
+
disabled = false,
|
|
117
|
+
showOrganisations = true,
|
|
118
|
+
showEvents = true
|
|
119
|
+
}: ContextSelectorProps) {
|
|
120
|
+
const {
|
|
121
|
+
organisations,
|
|
122
|
+
selectedOrganisation,
|
|
123
|
+
isLoading: orgLoading,
|
|
124
|
+
error: orgError,
|
|
125
|
+
refreshOrganisations
|
|
126
|
+
} = useOrganisations();
|
|
127
|
+
|
|
128
|
+
const {
|
|
129
|
+
events,
|
|
130
|
+
selectedEvent,
|
|
131
|
+
isLoading: eventLoading,
|
|
132
|
+
error: eventError,
|
|
133
|
+
refreshEvents
|
|
134
|
+
} = useEvents();
|
|
135
|
+
|
|
136
|
+
const { isSuperAdmin } = useRBAC();
|
|
137
|
+
|
|
138
|
+
const isLoading = (showOrganisations && orgLoading) || (showEvents && eventLoading);
|
|
139
|
+
const hasError = (showOrganisations && orgError) || (showEvents && eventError);
|
|
140
|
+
const hasItems =
|
|
141
|
+
(showOrganisations && (organisations?.length || 0) > 0) ||
|
|
142
|
+
(showEvents && (events?.length || 0) > 0);
|
|
143
|
+
|
|
144
|
+
// Determine current selection value
|
|
145
|
+
// Priority: Event selection takes precedence over organisation selection
|
|
146
|
+
// When an event is selected, show the event (even if an org is also selected)
|
|
147
|
+
// The organisation remains in context (derived from event) but selector shows the active selection
|
|
148
|
+
const currentValue = useMemo(() => {
|
|
149
|
+
if (showEvents && selectedEvent) {
|
|
150
|
+
return `event:${selectedEvent.event_id || selectedEvent.id}`;
|
|
151
|
+
}
|
|
152
|
+
if (showOrganisations && selectedOrganisation) {
|
|
153
|
+
return `org:${selectedOrganisation.id}`;
|
|
154
|
+
}
|
|
155
|
+
return '';
|
|
156
|
+
}, [showOrganisations, showEvents, selectedOrganisation?.id, selectedEvent]);
|
|
157
|
+
|
|
158
|
+
const handleValueChange = (value: string) => {
|
|
159
|
+
if (disabled || isLoading) return;
|
|
160
|
+
|
|
161
|
+
if (value.startsWith('org:') && showOrganisations) {
|
|
162
|
+
const orgId = value.replace('org:', '');
|
|
163
|
+
const org = organisations?.find(o => o.id === orgId);
|
|
164
|
+
if (org && onOrganisationSelect) {
|
|
165
|
+
onOrganisationSelect(org);
|
|
166
|
+
}
|
|
167
|
+
} else if (value.startsWith('event:') && showEvents) {
|
|
168
|
+
const eventId = value.replace('event:', '');
|
|
169
|
+
const event = events?.find(e => (e.event_id || e.id) === eventId);
|
|
170
|
+
if (event && onEventSelect) {
|
|
171
|
+
onEventSelect(event);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleRetry = async () => {
|
|
177
|
+
try {
|
|
178
|
+
if (showOrganisations && orgError) {
|
|
179
|
+
await refreshOrganisations();
|
|
180
|
+
}
|
|
181
|
+
if (showEvents && eventError) {
|
|
182
|
+
await refreshEvents();
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger.error('ContextSelector', 'Failed to refresh:', error);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Loading state - only show loading if we don't have any items yet
|
|
190
|
+
// This prevents the selector from being stuck in loading when data exists
|
|
191
|
+
if (isLoading && !hasItems) {
|
|
192
|
+
const loadingText = compact ? "Loading..." :
|
|
193
|
+
showOrganisations && showEvents ? "Loading organisations and events..." :
|
|
194
|
+
showOrganisations ? "Loading organisations..." :
|
|
195
|
+
"Loading events...";
|
|
196
|
+
return (
|
|
197
|
+
<div className={`flex items-center gap-2 ${className}`}>
|
|
198
|
+
<LoadingSpinner size="sm" />
|
|
199
|
+
<span className="text-sm text-muted-foreground">
|
|
200
|
+
{loadingText}
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Error state
|
|
207
|
+
if (hasError) {
|
|
208
|
+
const errorMessages = [];
|
|
209
|
+
if (showOrganisations && orgError) {
|
|
210
|
+
errorMessages.push(`Failed to load organisations: ${orgError.message}`);
|
|
211
|
+
}
|
|
212
|
+
if (showEvents && eventError) {
|
|
213
|
+
errorMessages.push(`Failed to load events: ${eventError.message}`);
|
|
214
|
+
}
|
|
215
|
+
return (
|
|
216
|
+
<div className={`space-y-2 ${className}`}>
|
|
217
|
+
<Alert variant="destructive">
|
|
218
|
+
<AlertCircle className="size-4" />
|
|
219
|
+
<AlertDescription>
|
|
220
|
+
{errorMessages.join(' ')}
|
|
221
|
+
</AlertDescription>
|
|
222
|
+
</Alert>
|
|
223
|
+
{showRetryButton && (
|
|
224
|
+
<Button
|
|
225
|
+
variant="outline"
|
|
226
|
+
size="sm"
|
|
227
|
+
onClick={handleRetry}
|
|
228
|
+
disabled={isLoading}
|
|
229
|
+
className="w-full"
|
|
230
|
+
>
|
|
231
|
+
<RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
|
232
|
+
Retry
|
|
233
|
+
</Button>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// No items available
|
|
240
|
+
if (!hasItems) {
|
|
241
|
+
if (showNoItemsMessage) {
|
|
242
|
+
const noItemsText =
|
|
243
|
+
showOrganisations && showEvents ? "No organisations or events available. Please contact your administrator." :
|
|
244
|
+
showOrganisations ? "No organisations available. Please contact your administrator." :
|
|
245
|
+
"No events available. Please contact your administrator.";
|
|
246
|
+
return (
|
|
247
|
+
<div className={`space-y-2 ${className}`}>
|
|
248
|
+
<Alert>
|
|
249
|
+
<AlertCircle className="size-4" />
|
|
250
|
+
<AlertDescription>
|
|
251
|
+
{noItemsText}
|
|
252
|
+
</AlertDescription>
|
|
253
|
+
</Alert>
|
|
254
|
+
{showRetryButton && (
|
|
255
|
+
<Button
|
|
256
|
+
variant="outline"
|
|
257
|
+
size="sm"
|
|
258
|
+
onClick={handleRetry}
|
|
259
|
+
disabled={isLoading}
|
|
260
|
+
className="w-full"
|
|
261
|
+
>
|
|
262
|
+
<RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
|
263
|
+
Check Again
|
|
264
|
+
</Button>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Format display value
|
|
273
|
+
// Priority: Event selection takes precedence over organisation selection (matches currentValue)
|
|
274
|
+
const displayValue = useMemo(() => {
|
|
275
|
+
if (showEvents && selectedEvent) {
|
|
276
|
+
return (
|
|
277
|
+
<div className="flex items-center gap-2">
|
|
278
|
+
<Calendar className="size-4 flex-shrink-0" />
|
|
279
|
+
<span className="truncate">{selectedEvent.event_name}</span>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (showOrganisations && selectedOrganisation) {
|
|
284
|
+
return (
|
|
285
|
+
<div className="flex items-center gap-2">
|
|
286
|
+
<Building2 className="size-4 flex-shrink-0" />
|
|
287
|
+
<span className="truncate">{selectedOrganisation.display_name}</span>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}, [showOrganisations, showEvents, selectedOrganisation, selectedEvent]);
|
|
293
|
+
|
|
294
|
+
// Determine placeholder text based on what's shown
|
|
295
|
+
const effectivePlaceholder = useMemo(() => {
|
|
296
|
+
if (placeholder !== "Select organisation or event") {
|
|
297
|
+
return placeholder;
|
|
298
|
+
}
|
|
299
|
+
if (showOrganisations && showEvents) {
|
|
300
|
+
return "Select organisation or event";
|
|
301
|
+
}
|
|
302
|
+
if (showOrganisations) {
|
|
303
|
+
return "Select organisation";
|
|
304
|
+
}
|
|
305
|
+
if (showEvents) {
|
|
306
|
+
return "Select event";
|
|
307
|
+
}
|
|
308
|
+
return placeholder;
|
|
309
|
+
}, [placeholder, showOrganisations, showEvents]);
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div className={className} data-testid="context-selector">
|
|
313
|
+
<Select
|
|
314
|
+
value={currentValue}
|
|
315
|
+
onValueChange={handleValueChange}
|
|
316
|
+
disabled={disabled || isLoading}
|
|
317
|
+
>
|
|
318
|
+
<SelectTrigger
|
|
319
|
+
className="text-left"
|
|
320
|
+
variant="outline"
|
|
321
|
+
>
|
|
322
|
+
<SelectValue placeholder={effectivePlaceholder}>
|
|
323
|
+
{displayValue}
|
|
324
|
+
</SelectValue>
|
|
325
|
+
</SelectTrigger>
|
|
326
|
+
<SelectContent>
|
|
327
|
+
{/* Organisations Section */}
|
|
328
|
+
{showOrganisations && organisations && organisations.length > 0 && (
|
|
329
|
+
<>
|
|
330
|
+
<SelectGroup>
|
|
331
|
+
<SelectLabel>Organisations</SelectLabel>
|
|
332
|
+
{organisations.map((org) => (
|
|
333
|
+
<SelectItem
|
|
334
|
+
key={org.id}
|
|
335
|
+
value={`org:${org.id}`}
|
|
336
|
+
>
|
|
337
|
+
<div className="flex items-center gap-2">
|
|
338
|
+
<Building2 className="size-4" />
|
|
339
|
+
<div className="flex flex-col">
|
|
340
|
+
<span className="font-medium">{org.display_name}</span>
|
|
341
|
+
{!compact && org.description && (
|
|
342
|
+
<span className="text-xs text-muted-foreground truncate max-w-40">
|
|
343
|
+
{org.description}
|
|
344
|
+
</span>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</SelectItem>
|
|
349
|
+
))}
|
|
350
|
+
</SelectGroup>
|
|
351
|
+
{showEvents && events && events.length > 0 && <SelectSeparator />}
|
|
352
|
+
</>
|
|
353
|
+
)}
|
|
354
|
+
|
|
355
|
+
{/* Events Section */}
|
|
356
|
+
{showEvents && events && events.length > 0 && (
|
|
357
|
+
<SelectGroup>
|
|
358
|
+
<SelectLabel>Events</SelectLabel>
|
|
359
|
+
{events.map((event) => (
|
|
360
|
+
<SelectItem
|
|
361
|
+
key={event.event_id || event.id}
|
|
362
|
+
value={`event:${event.event_id || event.id}`}
|
|
363
|
+
>
|
|
364
|
+
<div className="flex items-center gap-2">
|
|
365
|
+
<Calendar className="size-4" />
|
|
366
|
+
<div className="flex flex-col">
|
|
367
|
+
<span className="font-medium">{event.event_name}</span>
|
|
368
|
+
{!compact && event.event_date && (
|
|
369
|
+
<span className="text-xs text-muted-foreground">
|
|
370
|
+
{new Date(event.event_date).toLocaleDateString()}
|
|
371
|
+
</span>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</SelectItem>
|
|
376
|
+
))}
|
|
377
|
+
</SelectGroup>
|
|
378
|
+
)}
|
|
379
|
+
</SelectContent>
|
|
380
|
+
</Select>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
@@ -75,8 +75,7 @@ export interface RowProps {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const RowComponent =
|
|
79
|
-
({
|
|
78
|
+
const RowComponent = ({
|
|
80
79
|
row,
|
|
81
80
|
style,
|
|
82
81
|
isEditing,
|
|
@@ -138,6 +137,23 @@ const RowComponent = React.memo(
|
|
|
138
137
|
}
|
|
139
138
|
}, [isEditing, onSaveEditing, onCancelEditing]);
|
|
140
139
|
|
|
140
|
+
// CRITICAL: All hooks must be called before any early returns to avoid React hooks violations
|
|
141
|
+
// Move useMemo hooks here so they're always called, regardless of early returns below
|
|
142
|
+
const indentSize = hierarchical?.indentSize || 24;
|
|
143
|
+
const indentation = useMemo(() => {
|
|
144
|
+
return isChild && hierarchical?.state ? calculateIndentation(hierarchicalRow, [], indentSize) : 0;
|
|
145
|
+
}, [isChild, hierarchical?.state, hierarchicalRow, indentSize]);
|
|
146
|
+
|
|
147
|
+
const rowClassName = useMemo(() => {
|
|
148
|
+
if (isHierarchical) {
|
|
149
|
+
const hierarchicalClass = isParent
|
|
150
|
+
? hierarchical?.parentRowClassName || 'bg-main-50 hover:bg-main-100 font-medium'
|
|
151
|
+
: hierarchical?.childRowClassName || 'bg-sec-25 hover:bg-sec-50';
|
|
152
|
+
return cn(getTableRowClasses({ isSelected, isVirtualized: false }), hierarchicalClass);
|
|
153
|
+
}
|
|
154
|
+
return getTableRowClasses({ isSelected, isVirtualized: false });
|
|
155
|
+
}, [isHierarchical, isParent, isSelected, hierarchical]);
|
|
156
|
+
|
|
141
157
|
if (row.getIsGrouped && row.getIsGrouped()) {
|
|
142
158
|
const groupValue = row.getValue(grouping[0]);
|
|
143
159
|
const subRowsCount = row.subRows?.length || 0;
|
|
@@ -250,21 +266,6 @@ const RowComponent = React.memo(
|
|
|
250
266
|
);
|
|
251
267
|
}
|
|
252
268
|
|
|
253
|
-
const indentSize = hierarchical?.indentSize || 24;
|
|
254
|
-
const indentation = useMemo(() => {
|
|
255
|
-
return isChild && hierarchical?.state ? calculateIndentation(hierarchicalRow, [], indentSize) : 0;
|
|
256
|
-
}, [isChild, hierarchical?.state, hierarchicalRow, indentSize]);
|
|
257
|
-
|
|
258
|
-
const rowClassName = useMemo(() => {
|
|
259
|
-
if (isHierarchical) {
|
|
260
|
-
const hierarchicalClass = isParent
|
|
261
|
-
? hierarchical?.parentRowClassName || 'bg-main-50 hover:bg-main-100 font-medium'
|
|
262
|
-
: hierarchical?.childRowClassName || 'bg-sec-25 hover:bg-sec-50';
|
|
263
|
-
return cn(getTableRowClasses({ isSelected, isVirtualized: false }), hierarchicalClass);
|
|
264
|
-
}
|
|
265
|
-
return getTableRowClasses({ isSelected, isVirtualized: false });
|
|
266
|
-
}, [isHierarchical, isParent, isSelected, hierarchical]);
|
|
267
|
-
|
|
268
269
|
return (
|
|
269
270
|
<tr
|
|
270
271
|
ref={rowRef}
|
|
@@ -332,8 +333,7 @@ const RowComponent = React.memo(
|
|
|
332
333
|
})}
|
|
333
334
|
</tr>
|
|
334
335
|
);
|
|
335
|
-
}
|
|
336
|
-
);
|
|
336
|
+
};
|
|
337
337
|
|
|
338
338
|
RowComponent.displayName = 'RowComponent';
|
|
339
339
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import React, { useEffect, useRef } from 'react';
|
|
13
13
|
import { type Table } from '@tanstack/react-table';
|
|
14
|
-
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
14
|
+
import { useVirtualizer, type VirtualItem } from '@tanstack/react-virtual';
|
|
15
15
|
import { EmptyState } from './EmptyState';
|
|
16
16
|
import { FilterRow } from './FilterRow';
|
|
17
17
|
import { MemoizedRow } from './RowComponent';
|
|
@@ -194,7 +194,7 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
if (effectiveShouldVirtualize && virtualRows.length > 0) {
|
|
197
|
-
return virtualRows.map((virtualRow) => {
|
|
197
|
+
return virtualRows.map((virtualRow: VirtualItem) => {
|
|
198
198
|
const row = rows[virtualRow.index];
|
|
199
199
|
if (!row) return null;
|
|
200
200
|
|