@jmruthers/pace-core 0.5.183 → 0.5.185
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 +38 -0
- package/README.md +60 -1
- package/core-usage-manifest.json +312 -0
- package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
- package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
- package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-BABf6JCh.d.ts} +21 -10
- package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
- package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
- package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
- package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
- package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
- package/dist/{chunk-CSOFYHAG.js → chunk-AISXLWGZ.js} +374 -60
- package/dist/chunk-AISXLWGZ.js.map +1 -0
- package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
- package/dist/chunk-FXFJRTKI.js.map +1 -0
- package/dist/{chunk-QETLRQI6.js → chunk-HC67NW5K.js} +380 -360
- package/dist/chunk-HC67NW5K.js.map +1 -0
- package/dist/chunk-HESYZWZW.js +388 -0
- package/dist/chunk-HESYZWZW.js.map +1 -0
- package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
- package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
- package/dist/{chunk-UHNYIBXL.js → chunk-IXSNYUCT.js} +1 -1
- package/dist/chunk-IXSNYUCT.js.map +1 -0
- package/dist/{chunk-MI7HBHN3.js → chunk-MX3EIJGQ.js} +4 -3
- package/dist/{chunk-MI7HBHN3.js.map → chunk-MX3EIJGQ.js.map} +1 -1
- package/dist/{chunk-PWAHJW4G.js → chunk-OKI34GZD.js} +86 -33
- package/dist/chunk-OKI34GZD.js.map +1 -0
- package/dist/{chunk-W22JP75J.js → chunk-STTZQK2I.js} +3 -3
- package/dist/chunk-THRPYOFK.js +215 -0
- package/dist/chunk-THRPYOFK.js.map +1 -0
- package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
- package/dist/chunk-U6WNSFX5.js.map +1 -0
- package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
- package/dist/components.d.ts +182 -6
- package/dist/components.js +157 -11
- package/dist/components.js.map +1 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
- package/dist/{file-reference-D06mEEWW.d.ts → file-reference-BjR39ktt.d.ts} +7 -1
- package/dist/hooks.d.ts +7 -14
- package/dist/hooks.js +10 -22
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +79 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -1
- package/dist/rbac/index.d.ts +205 -14
- package/dist/rbac/index.js +28 -6
- package/dist/timezone-_pgH8qrY.d.ts +530 -0
- package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-CvnC3d-e.d.ts} +113 -2
- package/dist/utils.d.ts +109 -151
- package/dist/utils.js +128 -138
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +60 -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/Logger.md +178 -0
- 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/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +54 -0
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +18 -2
- 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/ComplianceResult.md +30 -0
- 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/DatabaseComplianceResult.md +85 -0
- package/docs/api/interfaces/DatabaseIssue.md +41 -0
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +6 -6
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.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 +24 -8
- package/docs/api/interfaces/FileUploadProps.md +24 -13
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
- 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/LoggerConfig.md +62 -0
- 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 +36 -23
- 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 +11 -11
- 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/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.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/QuickFix.md +52 -0
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +4 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +5 -5
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +41 -0
- 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/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.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/UseFormDialogOptions.md +62 -0
- package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.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 +738 -42
- package/docs/api-reference/hooks.md +111 -0
- package/docs/api-reference/rpc-functions.md +1 -1
- package/docs/api-reference/utilities.md +184 -0
- package/docs/getting-started/installation-guide.md +75 -16
- package/docs/getting-started/quick-start.md +61 -11
- package/docs/implementation-guides/authentication.md +88 -12
- package/docs/implementation-guides/file-reference-system.md +2 -1
- package/docs/implementation-guides/file-upload-storage.md +21 -0
- package/docs/rbac/README.md +1 -0
- package/docs/rbac/compliance/compliance-guide.md +544 -0
- package/docs/rbac/getting-started.md +158 -33
- package/docs/standards/pace-core-compliance.md +432 -0
- package/eslint-config-pace-core.cjs +93 -0
- package/package.json +15 -3
- package/scripts/analyze-bundle.js +232 -0
- package/scripts/build-css.js +56 -0
- package/scripts/build-docs-incremental.js +1015 -0
- package/scripts/check-pace-core-compliance.cjs +2353 -0
- package/scripts/generate-docs.js +157 -0
- package/scripts/setup-build-cache.js +73 -0
- package/scripts/utils/command-runner.js +131 -0
- package/scripts/utils/env.js +33 -0
- package/scripts/utils/index.js +10 -0
- package/scripts/utils/logger.js +88 -0
- package/scripts/utils/path-helpers.js +37 -0
- package/scripts/validate-formats.js +133 -0
- package/scripts/validate-master.js +155 -0
- package/scripts/validate-pre-publish.js +140 -0
- package/scripts/validate-theme.js +142 -0
- package/src/components/Calendar/Calendar.tsx +8 -1
- package/src/components/Card/Card.tsx +47 -8
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
- package/src/components/DatePickerWithTimezone/README.md +135 -0
- package/src/components/DatePickerWithTimezone/index.ts +10 -0
- package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
- package/src/components/DateTimeField/DateTimeField.tsx +232 -0
- package/src/components/DateTimeField/README.md +148 -0
- package/src/components/DateTimeField/index.ts +10 -0
- package/src/components/FileUpload/FileUpload.tsx +3 -0
- package/src/components/Header/Header.test.tsx +47 -18
- package/src/components/Header/Header.tsx +24 -6
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
- package/src/components/PaceAppLayout/README.md +9 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +12 -4
- package/src/components/index.ts +8 -0
- package/src/eslint-rules/pace-core-compliance.cjs +406 -0
- package/src/eslint-rules/pace-core-compliance.js +640 -0
- package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useFileReference.test.ts +1 -0
- package/src/hooks/useFormDialog.ts +147 -0
- package/src/index.ts +27 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
- package/src/rbac/compliance/database-validator.ts +165 -0
- package/src/rbac/compliance/index.ts +38 -0
- package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
- package/src/rbac/compliance/runtime-compliance.ts +77 -0
- package/src/rbac/compliance/setup-validator.ts +131 -0
- package/src/rbac/components/PagePermissionGuard.tsx +8 -64
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
- package/src/rbac/docs/event-based-apps.md +285 -0
- package/src/rbac/errors.ts +11 -0
- package/src/rbac/hooks/useRoleManagement.ts +292 -12
- package/src/rbac/index.ts +30 -0
- package/src/services/OrganisationService.ts +4 -0
- package/src/types/file-reference.ts +6 -0
- package/src/utils/__tests__/timezone.test.ts +345 -0
- package/src/utils/file-reference/__tests__/file-reference.test.ts +2 -0
- package/src/utils/file-reference/index.ts +1 -0
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
- package/src/utils/formatting/formatting.ts +179 -0
- package/src/utils/index.ts +27 -1
- package/src/utils/location/index.ts +16 -0
- package/src/utils/location/location.test.ts +286 -0
- package/src/utils/location/location.ts +175 -0
- package/src/utils/timezone/index.ts +17 -0
- package/src/utils/timezone/timezone.test.ts +349 -0
- package/src/utils/timezone/timezone.ts +281 -0
- package/dist/chunk-CSOFYHAG.js.map +0 -1
- package/dist/chunk-FUEYYMX5.js.map +0 -1
- package/dist/chunk-HKIT6O7W.js +0 -198
- package/dist/chunk-HKIT6O7W.js.map +0 -1
- package/dist/chunk-KUEN3HFB.js +0 -94
- package/dist/chunk-KUEN3HFB.js.map +0 -1
- package/dist/chunk-M7W4CP3M.js.map +0 -1
- package/dist/chunk-PWAHJW4G.js.map +0 -1
- package/dist/chunk-QETLRQI6.js.map +0 -1
- package/dist/chunk-UHNYIBXL.js.map +0 -1
- package/dist/formatting-5wETwiGF.d.ts +0 -162
- /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
- /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
- /package/dist/{chunk-W22JP75J.js.map → chunk-STTZQK2I.js.map} +0 -0
- /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
|
@@ -83,6 +83,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
83
83
|
const defaultAuthState = {
|
|
84
84
|
isAuthenticated: true,
|
|
85
85
|
authLoading: false,
|
|
86
|
+
isLoading: false, // Combined loading state used by component
|
|
86
87
|
};
|
|
87
88
|
|
|
88
89
|
const defaultSessionState = {
|
|
@@ -142,7 +143,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
142
143
|
expect(screen.queryByTestId('navigate')).not.toBeInTheDocument();
|
|
143
144
|
});
|
|
144
145
|
|
|
145
|
-
it('renders outlet when events exist but none selected (allows event selector visibility)', () => {
|
|
146
|
+
it('renders outlet when events exist but none selected (allows event selector visibility)', async () => {
|
|
146
147
|
const consoleDebugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
147
148
|
|
|
148
149
|
mockUseEvents.mockReturnValue({
|
|
@@ -151,12 +152,25 @@ describe('ProtectedRoute Component', () => {
|
|
|
151
152
|
isLoading: false,
|
|
152
153
|
});
|
|
153
154
|
|
|
154
|
-
|
|
155
|
+
// Wait for logger to be configured
|
|
156
|
+
await import('../../utils/core/logger').then(({ Logger, LogLevel }) => {
|
|
157
|
+
Logger.configure({
|
|
158
|
+
level: LogLevel.DEBUG,
|
|
159
|
+
includeTimestamp: false,
|
|
160
|
+
includeComponent: true,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
155
165
|
|
|
156
166
|
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
)
|
|
167
|
+
|
|
168
|
+
// Wait a bit for the logger to be called (it's called during render)
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
expect(consoleDebugSpy).toHaveBeenCalledWith(
|
|
171
|
+
expect.stringContaining('[DEBUG] [ProtectedRoute] Events available but none selected - allowing render so selector is visible')
|
|
172
|
+
);
|
|
173
|
+
}, { timeout: 1000 });
|
|
160
174
|
});
|
|
161
175
|
|
|
162
176
|
it('renders session restoration loader when session is restoring', () => {
|
|
@@ -178,6 +192,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
178
192
|
mockUseUnifiedAuth.mockReturnValue({
|
|
179
193
|
isAuthenticated: false,
|
|
180
194
|
authLoading: true,
|
|
195
|
+
isLoading: true, // Component uses isLoading, not authLoading
|
|
181
196
|
});
|
|
182
197
|
|
|
183
198
|
renderWithProviders(<ProtectedRoute />);
|
|
@@ -193,6 +208,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
193
208
|
mockUseUnifiedAuth.mockReturnValue({
|
|
194
209
|
isAuthenticated: false,
|
|
195
210
|
authLoading: true,
|
|
211
|
+
isLoading: true, // Component uses isLoading, not authLoading
|
|
196
212
|
});
|
|
197
213
|
|
|
198
214
|
renderWithProviders(<ProtectedRoute loadingFallback={customLoader} />);
|
|
@@ -207,6 +223,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
207
223
|
mockUseUnifiedAuth.mockReturnValue({
|
|
208
224
|
isAuthenticated: false,
|
|
209
225
|
authLoading: false,
|
|
226
|
+
isLoading: false,
|
|
210
227
|
});
|
|
211
228
|
|
|
212
229
|
renderWithProviders(<ProtectedRoute />);
|
|
@@ -222,6 +239,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
222
239
|
mockUseUnifiedAuth.mockReturnValue({
|
|
223
240
|
isAuthenticated: false,
|
|
224
241
|
authLoading: false,
|
|
242
|
+
isLoading: false,
|
|
225
243
|
});
|
|
226
244
|
|
|
227
245
|
renderWithProviders(<ProtectedRoute loginPath="/custom-login" />);
|
|
@@ -234,6 +252,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
234
252
|
mockUseUnifiedAuth.mockReturnValue({
|
|
235
253
|
isAuthenticated: false,
|
|
236
254
|
authLoading: false,
|
|
255
|
+
isLoading: false,
|
|
237
256
|
});
|
|
238
257
|
|
|
239
258
|
renderWithProviders(<ProtectedRoute />);
|
|
@@ -248,6 +267,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
248
267
|
mockUseUnifiedAuth.mockReturnValue({
|
|
249
268
|
isAuthenticated: false,
|
|
250
269
|
authLoading: false,
|
|
270
|
+
isLoading: false,
|
|
251
271
|
});
|
|
252
272
|
|
|
253
273
|
mockUseSessionRestoration.mockReturnValue({
|
|
@@ -273,6 +293,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
273
293
|
mockUseUnifiedAuth.mockReturnValue({
|
|
274
294
|
isAuthenticated: false,
|
|
275
295
|
authLoading: false,
|
|
296
|
+
isLoading: false,
|
|
276
297
|
});
|
|
277
298
|
|
|
278
299
|
mockUseSessionRestoration.mockReturnValue({
|
|
@@ -328,6 +349,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
328
349
|
mockUseUnifiedAuth.mockReturnValue({
|
|
329
350
|
isAuthenticated: false,
|
|
330
351
|
authLoading: true,
|
|
352
|
+
isLoading: true,
|
|
331
353
|
});
|
|
332
354
|
|
|
333
355
|
mockUseSessionRestoration.mockReturnValue({
|
|
@@ -347,6 +369,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
347
369
|
mockUseUnifiedAuth.mockReturnValue({
|
|
348
370
|
isAuthenticated: false,
|
|
349
371
|
authLoading: true,
|
|
372
|
+
isLoading: true,
|
|
350
373
|
});
|
|
351
374
|
|
|
352
375
|
mockUseSessionRestoration.mockReturnValue({
|
|
@@ -508,6 +531,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
508
531
|
mockUseUnifiedAuth.mockReturnValue({
|
|
509
532
|
isAuthenticated: true,
|
|
510
533
|
authLoading: false,
|
|
534
|
+
isLoading: false,
|
|
511
535
|
});
|
|
512
536
|
|
|
513
537
|
mockUseSessionRestoration.mockReturnValue({
|
|
@@ -527,6 +551,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
527
551
|
mockUseUnifiedAuth.mockReturnValue({
|
|
528
552
|
isAuthenticated: true,
|
|
529
553
|
authLoading: false,
|
|
554
|
+
isLoading: false,
|
|
530
555
|
});
|
|
531
556
|
|
|
532
557
|
mockUseSessionRestoration.mockReturnValue({
|
|
@@ -546,6 +571,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
546
571
|
mockUseUnifiedAuth.mockReturnValue({
|
|
547
572
|
isAuthenticated: true,
|
|
548
573
|
authLoading: true,
|
|
574
|
+
isLoading: true,
|
|
549
575
|
});
|
|
550
576
|
|
|
551
577
|
mockUseEvents.mockReturnValue({
|
|
@@ -576,7 +602,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
576
602
|
});
|
|
577
603
|
|
|
578
604
|
describe('Props Configuration', () => {
|
|
579
|
-
it('defaults requireEvent to
|
|
605
|
+
it('defaults requireEvent to false when not provided', () => {
|
|
580
606
|
mockUseEvents.mockReturnValue({
|
|
581
607
|
selectedEvent: null,
|
|
582
608
|
events: [],
|
|
@@ -585,8 +611,8 @@ describe('ProtectedRoute Component', () => {
|
|
|
585
611
|
|
|
586
612
|
renderWithProviders(<ProtectedRoute />);
|
|
587
613
|
|
|
588
|
-
// Should
|
|
589
|
-
expect(screen.getByTestId('
|
|
614
|
+
// Should render outlet since requireEvent defaults to false
|
|
615
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
590
616
|
});
|
|
591
617
|
|
|
592
618
|
it('respects requireEvent prop when set to false', () => {
|
|
@@ -607,6 +633,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
607
633
|
mockUseUnifiedAuth.mockReturnValue({
|
|
608
634
|
isAuthenticated: false,
|
|
609
635
|
authLoading: false,
|
|
636
|
+
isLoading: false,
|
|
610
637
|
});
|
|
611
638
|
|
|
612
639
|
renderWithProviders(<ProtectedRoute />);
|
|
@@ -619,6 +646,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
619
646
|
mockUseUnifiedAuth.mockReturnValue({
|
|
620
647
|
isAuthenticated: false,
|
|
621
648
|
authLoading: false,
|
|
649
|
+
isLoading: false,
|
|
622
650
|
});
|
|
623
651
|
|
|
624
652
|
renderWithProviders(<ProtectedRoute loginPath="/auth/login" />);
|
|
@@ -633,6 +661,7 @@ describe('ProtectedRoute Component', () => {
|
|
|
633
661
|
mockUseUnifiedAuth.mockReturnValue({
|
|
634
662
|
isAuthenticated: false,
|
|
635
663
|
authLoading: true,
|
|
664
|
+
isLoading: true,
|
|
636
665
|
});
|
|
637
666
|
|
|
638
667
|
renderWithProviders(<ProtectedRoute />);
|
|
@@ -131,14 +131,21 @@ export interface ProtectedRouteProps {
|
|
|
131
131
|
* @returns React element with route protection logic
|
|
132
132
|
*/
|
|
133
133
|
export function ProtectedRoute({
|
|
134
|
-
requireEvent =
|
|
134
|
+
requireEvent = false,
|
|
135
135
|
allowSuperAdminBypass = false,
|
|
136
136
|
noEventsFallback,
|
|
137
137
|
loadingFallback,
|
|
138
138
|
loginPath = '/login'
|
|
139
139
|
}: ProtectedRouteProps) {
|
|
140
|
-
const { isAuthenticated,
|
|
141
|
-
|
|
140
|
+
const { isAuthenticated, isLoading } = useUnifiedAuth();
|
|
141
|
+
|
|
142
|
+
// Always call useEvents() - UnifiedAuthProvider always includes EventServiceProvider
|
|
143
|
+
// Only use the values when requireEvent is true
|
|
144
|
+
const eventsContext = useEvents();
|
|
145
|
+
const selectedEvent = requireEvent ? eventsContext.selectedEvent : null;
|
|
146
|
+
const events = requireEvent ? (eventsContext.events || []) : [];
|
|
147
|
+
const eventLoading = requireEvent ? (eventsContext.isLoading || false) : false;
|
|
148
|
+
|
|
142
149
|
const sessionRestoration = useSessionRestoration();
|
|
143
150
|
|
|
144
151
|
const isRestoringSession = useMemo(() => {
|
|
@@ -165,7 +172,8 @@ export function ProtectedRoute({
|
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
// Show loading state while auth is being determined (but not organisation/event loading)
|
|
168
|
-
|
|
175
|
+
// Use isLoading (combined loading state) for consistency with simpler implementations
|
|
176
|
+
if (isLoading && !sessionRestoration.hasTimedOut) {
|
|
169
177
|
return loadingFallback || (
|
|
170
178
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
|
171
179
|
<LoadingSpinner />
|
package/src/components/index.ts
CHANGED
|
@@ -126,6 +126,14 @@ export type { TabsProps, TabsListProps, TabsTriggerProps, TabsContentProps } fro
|
|
|
126
126
|
export { Calendar } from './Calendar';
|
|
127
127
|
export type { CalendarProps } from './Calendar';
|
|
128
128
|
|
|
129
|
+
// DateTimeField exports
|
|
130
|
+
export { DateTimeField } from './DateTimeField';
|
|
131
|
+
export type { DateTimeFieldProps } from './DateTimeField';
|
|
132
|
+
|
|
133
|
+
// DatePickerWithTimezone exports
|
|
134
|
+
export { DatePickerWithTimezone } from './DatePickerWithTimezone';
|
|
135
|
+
export type { DatePickerWithTimezoneProps } from './DatePickerWithTimezone';
|
|
136
|
+
|
|
129
137
|
// Toast exports
|
|
130
138
|
export {
|
|
131
139
|
Toast,
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rules for pace-core Compliance Enforcement
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module ESLintRules/pace-core-compliance
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* This module provides ESLint rules to enforce pace-core usage patterns
|
|
8
|
+
* and prevent consuming apps from creating local alternatives.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Load manifest data
|
|
15
|
+
let manifestData = null;
|
|
16
|
+
try {
|
|
17
|
+
const manifestPath = path.join(__dirname, '../../core-usage-manifest.json');
|
|
18
|
+
if (fs.existsSync(manifestPath)) {
|
|
19
|
+
manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// If manifest can't be loaded, rules will use hardcoded defaults
|
|
23
|
+
console.warn('Warning: Could not load core-usage-manifest.json, using defaults');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get restricted imports from manifest or use defaults
|
|
27
|
+
const getRestrictedImports = () => {
|
|
28
|
+
if (manifestData && manifestData.restrictedImports) {
|
|
29
|
+
return manifestData.restrictedImports;
|
|
30
|
+
}
|
|
31
|
+
// Fallback defaults
|
|
32
|
+
return [
|
|
33
|
+
{ module: '@radix-ui/react-avatar', reason: 'Use Avatar component from pace-core instead' },
|
|
34
|
+
{ module: '@radix-ui/react-checkbox', reason: 'Use Checkbox component from pace-core instead' },
|
|
35
|
+
{ module: '@radix-ui/react-dialog', reason: 'Use Dialog component from pace-core instead' },
|
|
36
|
+
{ module: '@radix-ui/react-label', reason: 'Use Label component from pace-core instead' },
|
|
37
|
+
{ module: '@radix-ui/react-progress', reason: 'Use Progress component from pace-core instead' },
|
|
38
|
+
{ module: '@radix-ui/react-slot', reason: 'Use Button component from pace-core which handles slot composition' },
|
|
39
|
+
{ module: '@radix-ui/react-switch', reason: 'Use Switch component from pace-core instead' },
|
|
40
|
+
{ module: '@radix-ui/react-tabs', reason: 'Use Tabs component from pace-core instead' },
|
|
41
|
+
{ module: '@radix-ui/react-toast', reason: 'Use Toast component and useToast hook from pace-core instead' },
|
|
42
|
+
{ module: '@radix-ui/react-tooltip', reason: 'Use Tooltip component from pace-core instead' },
|
|
43
|
+
{ module: 'react-day-picker', reason: 'Use Calendar component from pace-core instead' },
|
|
44
|
+
{ module: '@tanstack/react-table', reason: 'Use DataTable component and related hooks from pace-core instead. DataTable wraps and standardizes table functionality' },
|
|
45
|
+
{ module: 'react-hook-form', reason: 'Use Form component and useZodForm hook from pace-core instead' },
|
|
46
|
+
{ module: 'zod', reason: 'Use validation utilities and schemas from pace-core instead. pace-core provides standardized validation helpers' }
|
|
47
|
+
];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Get pace-core components from manifest or use defaults
|
|
51
|
+
const getPaceCoreComponents = () => {
|
|
52
|
+
if (manifestData && manifestData.components) {
|
|
53
|
+
return manifestData.components;
|
|
54
|
+
}
|
|
55
|
+
return ['Button', 'Card', 'Dialog', 'Input', 'Form', 'Select', 'Alert', 'Badge', 'Checkbox', 'Switch', 'Textarea', 'Label', 'Table', 'DataTable', 'Toast', 'Tooltip', 'Tabs', 'Calendar', 'Avatar', 'Progress'];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Get pace-core hooks from manifest or use defaults
|
|
59
|
+
const getPaceCoreHooks = () => {
|
|
60
|
+
if (manifestData && manifestData.hooks) {
|
|
61
|
+
return manifestData.hooks;
|
|
62
|
+
}
|
|
63
|
+
return ['useToast', 'useDebounce', 'useUnifiedAuth', 'useEvents', 'useOrganisations', 'useFileReference', 'useStorage', 'useZodForm', 'useRBAC', 'usePermissions'];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Get pace-core utils from manifest or use defaults
|
|
67
|
+
const getPaceCoreUtils = () => {
|
|
68
|
+
if (manifestData && manifestData.utils) {
|
|
69
|
+
return manifestData.utils;
|
|
70
|
+
}
|
|
71
|
+
return ['formatDate', 'formatCurrency', 'formatNumber', 'formatTime', 'formatDateTime', 'cn', 'validateUserInput', 'sanitizeUserInput', 'hasPermission', 'getAppConfig'];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
rules: {
|
|
76
|
+
/**
|
|
77
|
+
* Block direct imports of libraries wrapped by pace-core
|
|
78
|
+
*/
|
|
79
|
+
'no-restricted-imports': {
|
|
80
|
+
meta: {
|
|
81
|
+
type: 'problem',
|
|
82
|
+
docs: {
|
|
83
|
+
description: 'Disallow direct imports of libraries that pace-core wraps',
|
|
84
|
+
category: 'Best Practices',
|
|
85
|
+
recommended: true
|
|
86
|
+
},
|
|
87
|
+
fixable: 'code',
|
|
88
|
+
hasSuggestions: true,
|
|
89
|
+
messages: {
|
|
90
|
+
restrictedImport: '{{message}} Import from {{alternative}} instead.',
|
|
91
|
+
restrictedImportWithReason: '{{message}} {{reason}}'
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
create(context) {
|
|
95
|
+
const restrictedImports = getRestrictedImports();
|
|
96
|
+
const restrictedModules = restrictedImports.map(imp => imp.module);
|
|
97
|
+
|
|
98
|
+
// Also catch @radix-ui/* patterns
|
|
99
|
+
const radixPattern = /^@radix-ui\//;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ImportDeclaration(node) {
|
|
103
|
+
const importSource = node.source.value;
|
|
104
|
+
|
|
105
|
+
// Check exact matches
|
|
106
|
+
const restricted = restrictedImports.find(imp => imp.module === importSource);
|
|
107
|
+
if (restricted) {
|
|
108
|
+
context.report({
|
|
109
|
+
node: node.source,
|
|
110
|
+
messageId: 'restrictedImportWithReason',
|
|
111
|
+
data: {
|
|
112
|
+
message: `Direct import of '${importSource}' is not allowed.`,
|
|
113
|
+
reason: restricted.reason
|
|
114
|
+
},
|
|
115
|
+
suggest: [{
|
|
116
|
+
desc: `Use pace-core alternative: ${restricted.reason}`,
|
|
117
|
+
fix(fixer) {
|
|
118
|
+
// Suggest importing from pace-core instead
|
|
119
|
+
const paceCoreAlternative = getPaceCoreAlternative(importSource);
|
|
120
|
+
if (paceCoreAlternative) {
|
|
121
|
+
return fixer.replaceText(
|
|
122
|
+
node.source,
|
|
123
|
+
`'@jmruthers/pace-core${paceCoreAlternative}'`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}]
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check @radix-ui/* pattern
|
|
134
|
+
if (radixPattern.test(importSource) && !restrictedModules.includes(importSource)) {
|
|
135
|
+
context.report({
|
|
136
|
+
node: node.source,
|
|
137
|
+
messageId: 'restrictedImport',
|
|
138
|
+
data: {
|
|
139
|
+
message: `Direct import of '${importSource}' is not allowed.`,
|
|
140
|
+
alternative: '@jmruthers/pace-core'
|
|
141
|
+
},
|
|
142
|
+
suggest: [{
|
|
143
|
+
desc: 'Use pace-core component instead',
|
|
144
|
+
fix(fixer) {
|
|
145
|
+
return fixer.replaceText(
|
|
146
|
+
node.source,
|
|
147
|
+
"'@jmruthers/pace-core'"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}]
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Prefer pace-core components over native HTML elements or custom implementations
|
|
160
|
+
*/
|
|
161
|
+
'prefer-pace-core-components': {
|
|
162
|
+
meta: {
|
|
163
|
+
type: 'suggestion',
|
|
164
|
+
docs: {
|
|
165
|
+
description: 'Suggest using pace-core components instead of native HTML elements',
|
|
166
|
+
category: 'Best Practices',
|
|
167
|
+
recommended: true
|
|
168
|
+
},
|
|
169
|
+
hasSuggestions: true,
|
|
170
|
+
messages: {
|
|
171
|
+
preferButton: "Use 'Button' component from '@jmruthers/pace-core' instead of <button>",
|
|
172
|
+
preferInput: "Use 'Input' component from '@jmruthers/pace-core' instead of <input>",
|
|
173
|
+
preferTextarea: "Use 'Textarea' component from '@jmruthers/pace-core' instead of <textarea>",
|
|
174
|
+
preferLabel: "Use 'Label' component from '@jmruthers/pace-core' instead of <label>",
|
|
175
|
+
preferForm: "Use 'Form' component from '@jmruthers/pace-core' instead of custom form implementation"
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
create(context) {
|
|
179
|
+
const paceCoreComponents = getPaceCoreComponents();
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
JSXOpeningElement(node) {
|
|
183
|
+
const elementName = node.name.name;
|
|
184
|
+
|
|
185
|
+
if (!elementName) return;
|
|
186
|
+
|
|
187
|
+
// Check for native HTML elements that have pace-core alternatives
|
|
188
|
+
const nativeToPaceCore = {
|
|
189
|
+
'button': 'Button',
|
|
190
|
+
'input': 'Input',
|
|
191
|
+
'textarea': 'Textarea',
|
|
192
|
+
'label': 'Label'
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (nativeToPaceCore[elementName.toLowerCase()]) {
|
|
196
|
+
const paceCoreComponent = nativeToPaceCore[elementName.toLowerCase()];
|
|
197
|
+
if (paceCoreComponents.includes(paceCoreComponent)) {
|
|
198
|
+
context.report({
|
|
199
|
+
node,
|
|
200
|
+
messageId: `prefer${paceCoreComponent}`,
|
|
201
|
+
suggest: [{
|
|
202
|
+
desc: `Import and use ${paceCoreComponent} from pace-core`,
|
|
203
|
+
fix(fixer) {
|
|
204
|
+
// This is a complex fix, so we'll just report
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}]
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Detect custom hooks that duplicate pace-core functionality
|
|
218
|
+
*/
|
|
219
|
+
'prefer-pace-core-hooks': {
|
|
220
|
+
meta: {
|
|
221
|
+
type: 'suggestion',
|
|
222
|
+
docs: {
|
|
223
|
+
description: 'Suggest using pace-core hooks instead of custom implementations',
|
|
224
|
+
category: 'Best Practices',
|
|
225
|
+
recommended: true
|
|
226
|
+
},
|
|
227
|
+
messages: {
|
|
228
|
+
preferPaceCoreHook: "Consider using '{{hook}}' from '@jmruthers/pace-core' instead of custom hook '{{customHook}}'"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
create(context) {
|
|
232
|
+
const paceCoreHooks = getPaceCoreHooks();
|
|
233
|
+
const hookPatterns = {
|
|
234
|
+
'useToast': ['useToast', 'useNotification', 'useSnackbar'],
|
|
235
|
+
'useDebounce': ['useDebounce', 'useDebounced'],
|
|
236
|
+
'useAuth': ['useAuth', 'useAuthentication', 'useUser'],
|
|
237
|
+
'useFile': ['useFile', 'useFileUpload', 'useFileReference'],
|
|
238
|
+
'useForm': ['useForm', 'useZodForm'],
|
|
239
|
+
'useTable': ['useTable', 'useDataTable']
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
FunctionDeclaration(node) {
|
|
244
|
+
const functionName = node.id?.name;
|
|
245
|
+
if (!functionName || !functionName.startsWith('use')) return;
|
|
246
|
+
|
|
247
|
+
// Check if this looks like a hook that pace-core provides
|
|
248
|
+
for (const [paceCoreHook, patterns] of Object.entries(hookPatterns)) {
|
|
249
|
+
if (paceCoreHooks.includes(paceCoreHook)) {
|
|
250
|
+
for (const pattern of patterns) {
|
|
251
|
+
if (functionName.toLowerCase().includes(pattern.toLowerCase().replace('use', ''))) {
|
|
252
|
+
context.report({
|
|
253
|
+
node: node.id,
|
|
254
|
+
messageId: 'preferPaceCoreHook',
|
|
255
|
+
data: {
|
|
256
|
+
hook: paceCoreHook,
|
|
257
|
+
customHook: functionName
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Detect utility functions that duplicate pace-core functionality
|
|
272
|
+
*/
|
|
273
|
+
'prefer-pace-core-utils': {
|
|
274
|
+
meta: {
|
|
275
|
+
type: 'suggestion',
|
|
276
|
+
docs: {
|
|
277
|
+
description: 'Suggest using pace-core utilities instead of custom implementations',
|
|
278
|
+
category: 'Best Practices',
|
|
279
|
+
recommended: true
|
|
280
|
+
},
|
|
281
|
+
messages: {
|
|
282
|
+
preferPaceCoreUtil: "Consider using '{{util}}' from '@jmruthers/pace-core' instead of custom function '{{customUtil}}'"
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
create(context) {
|
|
286
|
+
const paceCoreUtils = getPaceCoreUtils();
|
|
287
|
+
const utilPatterns = {
|
|
288
|
+
'formatDate': ['formatDate', 'formatDateTime', 'dateFormat'],
|
|
289
|
+
'formatCurrency': ['formatCurrency', 'formatMoney', 'currencyFormat'],
|
|
290
|
+
'formatNumber': ['formatNumber', 'numberFormat'],
|
|
291
|
+
'cn': ['cn', 'classNames', 'clsx', 'mergeClasses'],
|
|
292
|
+
'validateUserInput': ['validate', 'validateInput', 'validateUser'],
|
|
293
|
+
'sanitizeUserInput': ['sanitize', 'sanitizeInput', 'sanitizeUser']
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
FunctionDeclaration(node) {
|
|
298
|
+
const functionName = node.id?.name;
|
|
299
|
+
if (!functionName) return;
|
|
300
|
+
|
|
301
|
+
// Check if this looks like a util that pace-core provides
|
|
302
|
+
for (const [paceCoreUtil, patterns] of Object.entries(utilPatterns)) {
|
|
303
|
+
if (paceCoreUtils.includes(paceCoreUtil)) {
|
|
304
|
+
for (const pattern of patterns) {
|
|
305
|
+
if (functionName.toLowerCase().includes(pattern.toLowerCase())) {
|
|
306
|
+
context.report({
|
|
307
|
+
node: node.id,
|
|
308
|
+
messageId: 'preferPaceCoreUtil',
|
|
309
|
+
data: {
|
|
310
|
+
util: paceCoreUtil,
|
|
311
|
+
customUtil: functionName
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Detect component files with names matching pace-core components
|
|
326
|
+
*/
|
|
327
|
+
'no-local-component-duplication': {
|
|
328
|
+
meta: {
|
|
329
|
+
type: 'problem',
|
|
330
|
+
docs: {
|
|
331
|
+
description: 'Disallow local components with names matching pace-core components',
|
|
332
|
+
category: 'Best Practices',
|
|
333
|
+
recommended: true
|
|
334
|
+
},
|
|
335
|
+
messages: {
|
|
336
|
+
duplicateComponent: "Component '{{componentName}}' conflicts with pace-core component. Use '@jmruthers/pace-core' instead of creating a local version."
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
create(context) {
|
|
340
|
+
const paceCoreComponents = getPaceCoreComponents();
|
|
341
|
+
const filename = context.getFilename();
|
|
342
|
+
|
|
343
|
+
// Only check component files (components/, src/components/, etc.)
|
|
344
|
+
if (!filename.match(/(components|Components)\//)) {
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Extract component name from filename
|
|
349
|
+
const basename = path.basename(filename, path.extname(filename));
|
|
350
|
+
const componentName = basename.replace(/\.(test|spec)$/, '');
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
Program(node) {
|
|
354
|
+
// Check if this file exports a component with a name matching pace-core
|
|
355
|
+
if (paceCoreComponents.includes(componentName)) {
|
|
356
|
+
// Check if file exports this component
|
|
357
|
+
const hasExport = node.body.some(stmt => {
|
|
358
|
+
if (stmt.type === 'ExportNamedDeclaration') {
|
|
359
|
+
return stmt.declaration?.id?.name === componentName ||
|
|
360
|
+
stmt.specifiers?.some(spec => spec.exported.name === componentName);
|
|
361
|
+
}
|
|
362
|
+
if (stmt.type === 'ExportDefaultDeclaration') {
|
|
363
|
+
return stmt.declaration?.id?.name === componentName ||
|
|
364
|
+
stmt.declaration?.name === componentName;
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (hasExport) {
|
|
370
|
+
context.report({
|
|
371
|
+
node,
|
|
372
|
+
messageId: 'duplicateComponent',
|
|
373
|
+
data: {
|
|
374
|
+
componentName
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Helper function to get pace-core alternative for restricted imports
|
|
387
|
+
function getPaceCoreAlternative(importSource) {
|
|
388
|
+
const alternatives = {
|
|
389
|
+
'@radix-ui/react-avatar': '/components',
|
|
390
|
+
'@radix-ui/react-checkbox': '/components',
|
|
391
|
+
'@radix-ui/react-dialog': '/components',
|
|
392
|
+
'@radix-ui/react-label': '/components',
|
|
393
|
+
'@radix-ui/react-progress': '/components',
|
|
394
|
+
'@radix-ui/react-switch': '/components',
|
|
395
|
+
'@radix-ui/react-tabs': '/components',
|
|
396
|
+
'@radix-ui/react-toast': '/components',
|
|
397
|
+
'@radix-ui/react-tooltip': '/components',
|
|
398
|
+
'react-day-picker': '/components',
|
|
399
|
+
'@tanstack/react-table': '/components',
|
|
400
|
+
'react-hook-form': '/components',
|
|
401
|
+
'zod': '/utils'
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
return alternatives[importSource] || '';
|
|
405
|
+
}
|
|
406
|
+
|