@jmruthers/pace-core 0.6.1 → 0.6.3
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 +88 -10
- package/cursor-rules/00-pace-core-compliance.mdc +46 -87
- package/cursor-rules/01-standards-compliance.mdc +16 -47
- package/cursor-rules/02-project-structure.mdc +4 -4
- package/cursor-rules/03-solid-principles.mdc +45 -164
- package/cursor-rules/04-testing-standards.mdc +22 -69
- package/cursor-rules/05-bug-reports-and-features.mdc +2 -2
- package/cursor-rules/06-code-quality.mdc +42 -125
- package/cursor-rules/07-tech-stack-compliance.mdc +33 -128
- package/cursor-rules/08-markup-quality.mdc +452 -0
- package/cursor-rules/CHANGELOG.md +18 -0
- package/cursor-rules/README.md +2 -1
- package/dist/{AuthService-DjnJHDtC.d.ts → AuthService-Cb34EQs3.d.ts} +63 -1
- package/dist/{DataTable-CH1U5Tpy.d.ts → DataTable-BMRU8a1j.d.ts} +33 -1
- package/dist/{DataTable-DQ7RSOHE.js → DataTable-THFPBKTP.js} +12 -10
- package/dist/{PublicPageProvider-ce4xlHYA.d.ts → PublicPageProvider-DEMpysFR.d.ts} +394 -171
- package/dist/{UnifiedAuthProvider-185Ih4dj.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +30 -8
- package/dist/{UnifiedAuthProvider-ATAP5UTR.js → UnifiedAuthProvider-KAGUYQ4J.js} +5 -4
- package/dist/{api-N774RPUA.js → api-IAGWF3ZG.js} +10 -10
- package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
- package/dist/{chunk-JBKQ3SAO.js → chunk-2T2IG7T7.js} +107 -57
- package/dist/chunk-2T2IG7T7.js.map +1 -0
- package/dist/{chunk-3QRJFVBR.js → chunk-6SOIHG6Z.js} +1 -1
- package/dist/chunk-6SOIHG6Z.js.map +1 -0
- package/dist/{chunk-3XTALGJF.js → chunk-6Z7LTB3D.js} +69 -240
- package/dist/chunk-6Z7LTB3D.js.map +1 -0
- package/dist/{chunk-4ZC4GX36.js → chunk-CNCQDFLN.js} +199 -46
- package/dist/chunk-CNCQDFLN.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-BYFSK72L.js → chunk-DWUBLJJM.js} +361 -187
- package/dist/chunk-DWUBLJJM.js.map +1 -0
- package/dist/{chunk-LXQLPRQ2.js → chunk-FFQEQTNW.js} +6 -8
- package/dist/chunk-FFQEQTNW.js.map +1 -0
- package/dist/chunk-FMUCXFII.js +76 -0
- package/dist/chunk-FMUCXFII.js.map +1 -0
- package/dist/{chunk-4N5C5XZU.js → chunk-HFZBI76P.js} +4 -4
- package/dist/chunk-HFZBI76P.js.map +1 -0
- package/dist/{chunk-SQGMNID3.js → chunk-L4OXEN46.js} +4 -5
- package/dist/chunk-L4OXEN46.js.map +1 -0
- package/dist/{chunk-R77UEZ4E.js → chunk-M43Y4SSO.js} +1 -1
- package/dist/chunk-M43Y4SSO.js.map +1 -0
- package/dist/{chunk-I7PSE6JW.js → chunk-M7MPQISP.js} +3 -76
- package/dist/chunk-M7MPQISP.js.map +1 -0
- package/dist/chunk-PQBSKX33.js +7793 -0
- package/dist/chunk-PQBSKX33.js.map +1 -0
- package/dist/chunk-QRPVRXYT.js +226 -0
- package/dist/chunk-QRPVRXYT.js.map +1 -0
- package/dist/{chunk-KNC55RTG.js → chunk-RWEBCB47.js} +194 -416
- package/dist/chunk-RWEBCB47.js.map +1 -0
- package/dist/{chunk-XM25TVIE.js → chunk-YDQHOZNA.js} +843 -388
- package/dist/chunk-YDQHOZNA.js.map +1 -0
- package/dist/{chunk-GLK6VM3F.js → chunk-ZNIWI3UC.js} +739 -737
- package/dist/chunk-ZNIWI3UC.js.map +1 -0
- package/dist/components.d.ts +5 -5
- package/dist/components.js +18 -16
- package/dist/components.js.map +1 -1
- package/dist/contextValidator-3JNZKUTX.js +9 -0
- package/dist/contextValidator-3JNZKUTX.js.map +1 -0
- package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
- package/dist/{functions-D_kgHktt.d.ts → functions-DHebl8-F.d.ts} +1 -1
- package/dist/hooks.d.ts +55 -122
- package/dist/hooks.js +10 -13
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +60 -13
- package/dist/index.js +30 -25
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +21 -3
- package/dist/providers.js +4 -3
- package/dist/rbac/index.d.ts +210 -139
- package/dist/rbac/index.js +17 -13
- package/dist/styles/index.js +1 -1
- package/dist/theming/runtime.d.ts +1 -13
- package/dist/theming/runtime.js +2 -2
- package/dist/{timezone-_pgH8qrY.d.ts → timezone-CHhWg6b4.d.ts} +3 -10
- package/dist/{types-UU913iLA.d.ts → types-BeoeWV5I.d.ts} +8 -0
- package/dist/{types-CEpcvwwF.d.ts → types-CkbwOr4Y.d.ts} +6 -0
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-BJAlWfuJ.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +38 -17
- package/dist/utils.d.ts +4 -5
- package/dist/utils.js +17 -19
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +21 -17
- package/docs/api/modules.md +4191 -2967
- package/docs/architecture/database-schema-requirements.md +161 -0
- package/docs/components/context-selector.md +126 -0
- package/docs/core-concepts/rbac-system.md +3 -3
- package/docs/documentation-index.md +2 -4
- package/docs/getting-started/cursor-rules.md +2 -1
- package/docs/migration/DOCUMENTATION_STRUCTURE.md +441 -0
- package/docs/migration/MIGRATION_GUIDE.md +2 -24
- package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
- package/docs/migration/README.md +52 -6
- package/docs/migration/V0.5.190_TO_V0.6.1_MIGRATION.md +1153 -0
- package/docs/migration/database-changes-december-2025.md +3 -3
- package/docs/pace-mint-fix-auto-selection.md +218 -0
- package/docs/pace-mint-rbac-setup.md +391 -0
- package/docs/rbac/event-based-apps.md +1 -1
- package/docs/rbac/getting-started.md +1 -1
- package/docs/rbac/quick-start.md +1 -1
- package/docs/rbac/secure-client-protection.md +330 -0
- package/docs/standards/README.md +1 -0
- package/package.json +4 -3
- package/scripts/audit/core/checks/accessibility.cjs +197 -0
- package/scripts/audit/core/checks/api-usage.cjs +191 -0
- package/scripts/audit/core/checks/bundle.cjs +142 -0
- package/scripts/{check-pace-core-compliance.cjs → audit/core/checks/compliance.cjs} +784 -685
- package/scripts/audit/core/checks/config.cjs +54 -0
- package/scripts/audit/core/checks/coverage.cjs +84 -0
- package/scripts/audit/core/checks/dependencies.cjs +985 -0
- package/scripts/audit/core/checks/documentation.cjs +268 -0
- package/scripts/audit/core/checks/environment.cjs +116 -0
- package/scripts/audit/core/checks/error-handling.cjs +340 -0
- package/scripts/audit/core/checks/forms.cjs +172 -0
- package/scripts/audit/core/checks/heuristics.cjs +68 -0
- package/scripts/audit/core/checks/hooks.cjs +334 -0
- package/scripts/audit/core/checks/imports.cjs +244 -0
- package/scripts/audit/core/checks/performance.cjs +325 -0
- package/scripts/audit/core/checks/routes.cjs +117 -0
- package/scripts/audit/core/checks/state.cjs +130 -0
- package/scripts/audit/core/checks/structure.cjs +65 -0
- package/scripts/audit/core/checks/style.cjs +584 -0
- package/scripts/audit/core/checks/testing.cjs +122 -0
- package/scripts/audit/core/checks/typescript.cjs +61 -0
- package/scripts/audit/core/scanner.cjs +199 -0
- package/scripts/audit/core/utils.cjs +137 -0
- package/scripts/audit/index.cjs +223 -0
- package/scripts/audit/reporters/console.cjs +151 -0
- package/scripts/audit/reporters/json.cjs +54 -0
- package/scripts/audit/reporters/markdown.cjs +124 -0
- package/scripts/audit-consuming-app.cjs +61 -936
- package/scripts/build-docs/build-decision.js +240 -0
- package/scripts/build-docs/cache-utils.js +105 -0
- package/scripts/build-docs/content-normalization.js +150 -0
- package/scripts/build-docs/file-utils.js +105 -0
- package/scripts/build-docs/git-utils.js +86 -0
- package/scripts/build-docs/hash-utils.js +116 -0
- package/scripts/build-docs/typedoc-runner.js +220 -0
- package/scripts/build-docs-incremental.js +77 -913
- package/scripts/utils/command-runner.js +16 -11
- package/scripts/validate-formats.js +61 -56
- package/scripts/validate-master.js +74 -69
- package/scripts/validate-pre-publish.js +70 -65
- package/src/__tests__/hooks/usePermissions.test.ts +2 -2
- package/src/components/Alert/Alert.test.tsx +12 -18
- package/src/components/Alert/Alert.tsx +5 -7
- package/src/components/Avatar/Avatar.test.tsx +4 -4
- package/src/components/Badge/Badge.tsx +14 -0
- package/src/components/Button/Button.tsx +22 -0
- package/src/components/Calendar/Calendar.tsx +8 -2
- package/src/components/Card/Card.tsx +4 -0
- package/src/components/Checkbox/Checkbox.test.tsx +12 -12
- package/src/components/Checkbox/Checkbox.tsx +2 -2
- package/src/components/ContextSelector/ContextSelector.tsx +384 -0
- package/src/components/ContextSelector/index.ts +3 -0
- package/src/components/DataTable/DataTable.tsx +38 -4
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +5 -6
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +18 -4
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +2 -3
- package/src/components/DataTable/components/AccessDeniedPage.tsx +16 -25
- package/src/components/DataTable/components/ActionButtons.tsx +10 -7
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +1 -1
- package/src/components/DataTable/components/ColumnFilter.tsx +10 -0
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +12 -0
- package/src/components/DataTable/components/DataTableBody.tsx +8 -0
- package/src/components/DataTable/components/DataTableCore.tsx +196 -554
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +11 -0
- package/src/components/DataTable/components/DataTableLayout.tsx +559 -0
- package/src/components/DataTable/components/DataTableModals.tsx +8 -0
- package/src/components/DataTable/components/DataTableToolbar.tsx +8 -0
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +12 -0
- package/src/components/DataTable/components/EditFields.tsx +307 -0
- package/src/components/DataTable/components/EditableRow.tsx +8 -0
- package/src/components/DataTable/components/EmptyState.tsx +10 -0
- package/src/components/DataTable/components/FilterRow.tsx +12 -0
- package/src/components/DataTable/components/GroupHeader.tsx +12 -0
- package/src/components/DataTable/components/GroupingDropdown.tsx +12 -0
- package/src/components/DataTable/components/ImportModal.tsx +7 -0
- package/src/components/DataTable/components/LoadingState.tsx +6 -0
- package/src/components/DataTable/components/PaginationControls.tsx +16 -1
- package/src/components/DataTable/components/RowComponent.tsx +391 -0
- package/src/components/DataTable/components/UnifiedTableBody.tsx +63 -851
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +16 -4
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +4 -2
- package/src/components/DataTable/components/cellValueUtils.ts +40 -0
- package/src/components/DataTable/components/hooks/useImportModalFocus.ts +53 -0
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +126 -0
- package/src/components/DataTable/context/DataTableContext.tsx +50 -0
- package/src/components/DataTable/core/ColumnFactory.ts +31 -0
- package/src/components/DataTable/core/DataTableContext.tsx +32 -1
- package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +10 -0
- package/src/components/DataTable/hooks/useColumnReordering.ts +12 -0
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +10 -0
- package/src/components/DataTable/hooks/useDataTableDataPipeline.ts +16 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +127 -33
- package/src/components/DataTable/hooks/useDataTableState.ts +35 -1
- package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +12 -0
- package/src/components/DataTable/hooks/useServerSideDataEffect.ts +11 -0
- package/src/components/DataTable/hooks/useTableColumns.ts +8 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +14 -0
- package/src/components/DataTable/styles.ts +6 -6
- package/src/components/DataTable/types.ts +6 -10
- package/src/components/DataTable/utils/a11yUtils.ts +7 -0
- package/src/components/DataTable/utils/debugTools.ts +18 -113
- package/src/components/DataTable/utils/errorHandling.ts +12 -0
- package/src/components/DataTable/utils/exportUtils.ts +9 -0
- package/src/components/DataTable/utils/flexibleImport.ts +12 -48
- package/src/components/DataTable/utils/paginationUtils.ts +8 -0
- package/src/components/DataTable/utils/performanceUtils.ts +5 -1
- package/src/components/Dialog/Dialog.tsx +31 -3
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +180 -1
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +45 -5
- package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +129 -0
- package/src/components/ErrorBoundary/index.ts +27 -2
- package/src/components/FileDisplay/FileDisplay.tsx +74 -28
- package/src/components/FileUpload/FileUpload.tsx +22 -2
- package/src/components/Footer/Footer.test.tsx +16 -16
- package/src/components/Footer/Footer.tsx +14 -11
- package/src/components/Form/Form.tsx +1 -0
- package/src/components/Header/Header.test.tsx +43 -73
- package/src/components/Header/Header.tsx +59 -49
- package/src/components/Input/Input.test.tsx +2 -2
- package/src/components/Input/Input.tsx +8 -4
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +4 -4
- package/src/components/LoginForm/LoginForm.tsx +4 -0
- package/src/components/NavigationMenu/NavigationMenu.tsx +14 -513
- package/src/components/NavigationMenu/types.ts +56 -0
- package/src/components/NavigationMenu/useNavigationFiltering.ts +390 -0
- 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 +13 -11
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +167 -44
- package/src/components/PaceAppLayout/README.md +14 -17
- package/src/components/PaceAppLayout/test-setup.tsx +3 -4
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +3 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +9 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +3 -9
- package/src/components/PublicLayout/PublicPageLayout.tsx +2 -5
- package/src/components/PublicLayout/PublicPageProvider.tsx +4 -0
- package/src/components/Select/Select.tsx +80 -434
- package/src/components/Select/context.ts +23 -0
- package/src/components/Select/hooks/useSelectEvents.ts +87 -0
- package/src/components/Select/hooks/useSelectSearch.ts +91 -0
- package/src/components/Select/hooks/useSelectState.ts +104 -0
- package/src/components/Select/index.ts +9 -1
- package/src/components/Select/types.ts +123 -0
- package/src/components/Select/utils/text.ts +26 -0
- package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +4 -5
- package/src/components/Switch/Switch.tsx +4 -4
- package/src/components/Tabs/Tabs.tsx +1 -1
- package/src/components/Toast/Toast.tsx +4 -0
- package/src/components/Tooltip/Tooltip.tsx +2 -2
- package/src/components/UserMenu/UserMenu.test.tsx +24 -11
- package/src/components/UserMenu/UserMenu.tsx +21 -18
- package/src/components/index.ts +7 -7
- package/src/eslint-rules/pace-core-compliance.cjs +106 -0
- package/src/hooks/__tests__/index.unit.test.ts +2 -5
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
- package/src/hooks/index.ts +1 -2
- package/src/hooks/public/usePublicEvent.ts +4 -0
- package/src/hooks/public/usePublicEventLogo.ts +4 -0
- package/src/hooks/public/usePublicFileDisplay.ts +4 -0
- package/src/hooks/public/usePublicRouteParams.ts +4 -0
- package/src/hooks/services/useAuth.ts +32 -0
- package/src/hooks/services/useCurrentEvent.ts +6 -0
- package/src/hooks/services/useCurrentOrganisation.ts +6 -0
- package/src/hooks/useAppConfig.ts +15 -30
- package/src/hooks/useDebounce.ts +9 -0
- package/src/hooks/useEventTheme.ts +6 -0
- package/src/hooks/useFileDisplay.ts +81 -50
- package/src/hooks/useFileReference.ts +25 -7
- package/src/hooks/useFileUrl.ts +11 -1
- package/src/hooks/useFocusManagement.ts +14 -0
- package/src/hooks/useFocusTrap.ts +3 -0
- package/src/hooks/useInactivityTracker.ts +3 -0
- package/src/hooks/useKeyboardShortcuts.ts +4 -0
- package/src/hooks/useOrganisationPermissions.ts +4 -0
- package/src/hooks/useOrganisationSecurity.ts +4 -0
- package/src/hooks/usePerformanceMonitor.ts +4 -0
- package/src/hooks/usePermissionCache.ts +7 -0
- package/src/hooks/useQueryCache.ts +12 -1
- package/src/hooks/useSessionRestoration.ts +4 -0
- package/src/hooks/useStorage.ts +4 -0
- package/src/hooks/useToast.ts +1 -1
- package/src/index.ts +6 -6
- package/src/providers/__tests__/OrganisationProvider.test.tsx +92 -70
- package/src/providers/services/AuthServiceProvider.tsx +35 -7
- package/src/providers/services/EventServiceProvider.tsx +51 -5
- package/src/providers/services/InactivityServiceProvider.tsx +18 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +18 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +126 -134
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +29 -13
- package/src/rbac/README.md +1 -1
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +4 -5
- package/src/rbac/adapters.tsx +12 -3
- package/src/rbac/api.test.ts +59 -51
- package/src/rbac/api.ts +246 -167
- package/src/rbac/components/NavigationProvider.tsx +4 -1
- package/src/rbac/components/PagePermissionGuard.tsx +185 -17
- package/src/rbac/components/RoleBasedRouter.tsx +5 -1
- package/src/rbac/components/SecureDataProvider.test.tsx +84 -49
- package/src/rbac/components/SecureDataProvider.tsx +20 -5
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +24 -14
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +7 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +14 -6
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +15 -4
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +148 -24
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +81 -15
- package/src/rbac/engine.ts +38 -14
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
- package/src/rbac/hooks/permissions/index.ts +7 -0
- package/src/rbac/hooks/permissions/useAccessLevel.ts +105 -0
- package/src/rbac/hooks/permissions/useCachedPermissions.ts +79 -0
- package/src/rbac/hooks/permissions/useCan.ts +377 -0
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +90 -0
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +90 -0
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +93 -0
- package/src/rbac/hooks/permissions/usePermissions.ts +253 -0
- package/src/rbac/hooks/useCan.test.ts +64 -66
- package/src/rbac/hooks/usePermissions.ts +14 -995
- 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/useResourcePermissions.test.ts +54 -18
- package/src/rbac/hooks/useResourcePermissions.ts +14 -4
- package/src/rbac/hooks/useSecureSupabase.ts +27 -7
- package/src/rbac/index.ts +7 -0
- package/src/rbac/permissions.ts +0 -30
- package/src/rbac/secureClient.test.ts +22 -18
- package/src/rbac/secureClient.ts +294 -68
- package/src/rbac/security.ts +0 -17
- package/src/rbac/types.ts +9 -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 +186 -54
- 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/theming/__tests__/parseEventColours.test.ts +6 -9
- package/src/theming/parseEventColours.ts +5 -19
- package/src/types/vitest-globals.d.ts +51 -26
- package/src/utils/__mocks__/supabaseMock.ts +1 -3
- package/src/utils/__tests__/formatting.unit.test.ts +4 -4
- package/src/utils/__tests__/index.unit.test.ts +2 -2
- package/src/utils/audit/audit.ts +0 -3
- package/src/utils/core/cn.ts +1 -1
- package/src/utils/dynamic/dynamicUtils.ts +7 -4
- package/src/utils/file-reference/index.ts +53 -1
- package/src/utils/formatting/formatting.ts +8 -18
- package/src/utils/index.ts +0 -1
- package/dist/chunk-3QRJFVBR.js.map +0 -1
- package/dist/chunk-3XTALGJF.js.map +0 -1
- package/dist/chunk-4N5C5XZU.js.map +0 -1
- package/dist/chunk-4ZC4GX36.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-BYFSK72L.js.map +0 -1
- package/dist/chunk-EXUD6RNJ.js +0 -451
- package/dist/chunk-EXUD6RNJ.js.map +0 -1
- package/dist/chunk-GLK6VM3F.js.map +0 -1
- package/dist/chunk-I7PSE6JW.js.map +0 -1
- package/dist/chunk-JBKQ3SAO.js.map +0 -1
- package/dist/chunk-KNC55RTG.js.map +0 -1
- package/dist/chunk-LXQLPRQ2.js.map +0 -1
- package/dist/chunk-R77UEZ4E.js.map +0 -1
- package/dist/chunk-SQGMNID3.js.map +0 -1
- package/dist/chunk-T33XF5ZC.js +0 -12922
- package/dist/chunk-T33XF5ZC.js.map +0 -1
- package/dist/chunk-XM25TVIE.js.map +0 -1
- package/docs/api/classes/ColumnFactory.md +0 -243
- package/docs/api/classes/ErrorBoundary.md +0 -144
- 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 -160
- 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 -27
- package/docs/api/interfaces/ButtonProps.md +0 -53
- package/docs/api/interfaces/CalendarProps.md +0 -70
- package/docs/api/interfaces/CardProps.md +0 -66
- 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 -249
- 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/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 -293
- package/docs/api/interfaces/FooterProps.md +0 -105
- 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 -53
- package/docs/api/interfaces/LabelProps.md +0 -107
- package/docs/api/interfaces/LoggerConfig.md +0 -62
- package/docs/api/interfaces/LoginFormProps.md +0 -184
- 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 -406
- package/docs/api/interfaces/PaceLoginPageProps.md +0 -47
- 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 -97
- package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
- package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
- package/docs/api/interfaces/PublicPageLayoutProps.md +0 -198
- 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 -9
- package/docs/api/interfaces/ToastProps.md +0 -9
- package/docs/api/interfaces/UnifiedAuthContextType.md +0 -820
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -171
- package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
- package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -136
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -81
- package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
- package/docs/api/interfaces/UsePublicEventReturn.md +0 -68
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -120
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -94
- 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 -118
- package/docs/api/interfaces/UserMenuProps.md +0 -86
- package/docs/api/interfaces/UserProfile.md +0 -63
- package/docs/migration/quick-migration-guide.md +0 -356
- package/docs/migration/service-architecture.md +0 -281
- package/src/components/EventSelector/EventSelector.test.tsx +0 -720
- package/src/components/EventSelector/EventSelector.tsx +0 -420
- package/src/components/EventSelector/index.ts +0 -3
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -324
- package/src/components/OrganisationSelector/index.ts +0 -9
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +0 -680
- package/src/hooks/useSecureDataAccess.test.ts +0 -559
- package/src/hooks/useSecureDataAccess.ts +0 -681
- /package/dist/{DataTable-DQ7RSOHE.js.map → DataTable-THFPBKTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-ATAP5UTR.js.map → UnifiedAuthProvider-KAGUYQ4J.js.map} +0 -0
- /package/dist/{api-N774RPUA.js.map → api-IAGWF3ZG.js.map} +0 -0
- /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/docs/migration/{organisation-context-timing-fix.md → V0.3.44_organisation-context-timing-fix.md} +0 -0
- /package/docs/migration/{rbac-migration.md → V0.4.0_rbac-migration.md} +0 -0
- /package/docs/migration/{person-scoped-profiles-migration-guide.md → V0.5.190_person-scoped-profiles-migration-guide.md} +0 -0
- /package/docs/migration/{REACT_19_MIGRATION.md → V0.6.0_REACT_19_MIGRATION.md} +0 -0
|
@@ -3,45 +3,35 @@
|
|
|
3
3
|
* @package @jmruthers/pace-core
|
|
4
4
|
* @module Components/DataTable/Components
|
|
5
5
|
* @since 0.3.0
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* A unified table body component that handles both standard and virtualized rendering
|
|
8
8
|
* based on data size. This eliminates the need for separate virtualized components
|
|
9
9
|
* while maintaining all features consistently.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import React, {
|
|
13
|
-
import { type Table
|
|
14
|
-
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
15
|
-
// Removed Table component imports - using native HTML elements
|
|
16
|
-
import { Button } from '../../Button/Button';
|
|
17
|
-
import { ChevronUp, ChevronDown, ChevronRight } from 'lucide-react';
|
|
12
|
+
import React, { useEffect, useRef } from 'react';
|
|
13
|
+
import { type Table } from '@tanstack/react-table';
|
|
14
|
+
import { useVirtualizer, type VirtualItem } from '@tanstack/react-virtual';
|
|
18
15
|
import { EmptyState } from './EmptyState';
|
|
19
16
|
import { FilterRow } from './FilterRow';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { getTableCellClasses
|
|
17
|
+
import { MemoizedRow } from './RowComponent';
|
|
18
|
+
import { renderEditField } from './EditFields';
|
|
19
|
+
import { getTableCellClasses } from '../styles';
|
|
23
20
|
import { Input } from '../../Input/Input';
|
|
24
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel, SelectSeparator } from '../../Select/Select';
|
|
25
21
|
import { createLogger } from '../../../utils/core/logger';
|
|
26
|
-
import { cn } from '../../../utils/core/cn';
|
|
27
22
|
import type {
|
|
28
23
|
AggregateConfig,
|
|
24
|
+
CellValue,
|
|
29
25
|
DataRecord,
|
|
30
26
|
DataTableAction,
|
|
31
|
-
DataTableColumn,
|
|
32
27
|
EditableColumnDef,
|
|
33
28
|
EmptyStateConfig,
|
|
34
29
|
HierarchicalConfig,
|
|
35
|
-
HierarchicalDataRow,
|
|
36
|
-
CellValue
|
|
37
30
|
} from '../types';
|
|
38
|
-
import { calculateIndentation } from '../utils/hierarchicalUtils';
|
|
39
31
|
import { getRowIdSafe } from '../utils/rowUtils';
|
|
40
32
|
|
|
41
33
|
// Performance thresholds
|
|
42
34
|
const VIRTUALIZATION_THRESHOLD = 1000;
|
|
43
|
-
const CHUNKING_THRESHOLD = 10000;
|
|
44
|
-
const MEMORY_OPTIMIZATION_THRESHOLD = 100000;
|
|
45
35
|
|
|
46
36
|
/**
|
|
47
37
|
* Props for the unified table body component
|
|
@@ -123,752 +113,6 @@ interface UnifiedTableBodyProps<TData extends DataRecord> {
|
|
|
123
113
|
};
|
|
124
114
|
}
|
|
125
115
|
|
|
126
|
-
// Component for select fields with searchable and creatable support
|
|
127
|
-
function SelectEditField<TData extends DataRecord>({
|
|
128
|
-
columnDef,
|
|
129
|
-
accessorKey,
|
|
130
|
-
currentValue,
|
|
131
|
-
placeholder,
|
|
132
|
-
onChange,
|
|
133
|
-
}: {
|
|
134
|
-
columnDef: EditableColumnDef<TData>;
|
|
135
|
-
accessorKey: string;
|
|
136
|
-
currentValue: CellValue;
|
|
137
|
-
placeholder?: string;
|
|
138
|
-
onChange: (value: CellValue) => void;
|
|
139
|
-
}) {
|
|
140
|
-
const logger = createLogger('SelectEditField');
|
|
141
|
-
// Determine if searchable - explicitly check for true to ensure visible search input appears
|
|
142
|
-
// When selectSearchable is true or undefined, show the visible search input box
|
|
143
|
-
// When selectSearchable is false, hide the search input (type-to-search still works via SelectContent internals)
|
|
144
|
-
const isSearchable = columnDef.selectSearchable !== false;
|
|
145
|
-
const isCreatable = columnDef.creatable === true;
|
|
146
|
-
const selectRef = React.useRef<HTMLFormElement>(null);
|
|
147
|
-
const [searchTerm, setSearchTerm] = React.useState('');
|
|
148
|
-
const [isOpen, setIsOpen] = React.useState(false);
|
|
149
|
-
const [showCreateOption, setShowCreateOption] = React.useState(false);
|
|
150
|
-
|
|
151
|
-
// Monitor search input value via DOM events to detect when user types
|
|
152
|
-
React.useEffect(() => {
|
|
153
|
-
if (!isOpen || !isSearchable || !isCreatable || !columnDef.onCreateNew) {
|
|
154
|
-
// Reset showCreateOption when conditions aren't met
|
|
155
|
-
if (!isOpen || !isCreatable || !columnDef.onCreateNew) {
|
|
156
|
-
setShowCreateOption(false);
|
|
157
|
-
}
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Function to find and attach listener to search input
|
|
162
|
-
const findAndAttachSearchInput = (): (() => void) | null => {
|
|
163
|
-
// Try to find search input - check both within selectRef and document
|
|
164
|
-
// SelectContent might be rendered outside the form element
|
|
165
|
-
let searchInput: HTMLInputElement | null = null;
|
|
166
|
-
|
|
167
|
-
if (selectRef.current) {
|
|
168
|
-
searchInput = selectRef.current.querySelector<HTMLInputElement>('[data-testid="select-search-input"]');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// If not found in selectRef, try document (in case SelectContent is in a portal)
|
|
172
|
-
if (!searchInput) {
|
|
173
|
-
// Find the most recently opened select's search input
|
|
174
|
-
const allSearchInputs = document.querySelectorAll<HTMLInputElement>('[data-testid="select-search-input"]');
|
|
175
|
-
// Get the one that's visible (not hidden)
|
|
176
|
-
for (const input of Array.from(allSearchInputs)) {
|
|
177
|
-
const content = input.closest('[data-testid="select-content"]');
|
|
178
|
-
if (content && content.getAttribute('aria-hidden') !== 'true') {
|
|
179
|
-
searchInput = input;
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (!searchInput) return null;
|
|
186
|
-
|
|
187
|
-
const handleInput = (e: Event) => {
|
|
188
|
-
const target = e.target as HTMLInputElement;
|
|
189
|
-
const currentSearch = target.value;
|
|
190
|
-
setSearchTerm(currentSearch);
|
|
191
|
-
|
|
192
|
-
// Check if search doesn't match any option (including items in groups)
|
|
193
|
-
if (currentSearch.trim()) {
|
|
194
|
-
const searchLower = currentSearch.toLowerCase().trim();
|
|
195
|
-
|
|
196
|
-
// Helper to check if an option matches
|
|
197
|
-
// Use explicit union type instead of typeof to avoid Babel parsing issues
|
|
198
|
-
type FieldOption =
|
|
199
|
-
| { value: string | number; label: string }
|
|
200
|
-
| { type: 'group'; label: string; items: Array<{ value: string | number; label: string }> }
|
|
201
|
-
| { type: 'separator' };
|
|
202
|
-
|
|
203
|
-
const checkMatch = (opt: FieldOption): boolean => {
|
|
204
|
-
// Simple option
|
|
205
|
-
if ('value' in opt && !('type' in opt)) {
|
|
206
|
-
return opt.label.toLowerCase().includes(searchLower);
|
|
207
|
-
}
|
|
208
|
-
// Group - check items within the group
|
|
209
|
-
if ('type' in opt && opt.type === 'group') {
|
|
210
|
-
return (opt as { type: 'group'; label: string; items: Array<{ value: string | number; label: string }> }).items.some((item: { value: string | number; label: string }) => item.label.toLowerCase().includes(searchLower));
|
|
211
|
-
}
|
|
212
|
-
// Separator - doesn't match
|
|
213
|
-
return false;
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const hasMatch = (columnDef.fieldOptions || []).some(checkMatch);
|
|
217
|
-
// Always show create option when there's a search term (user might want to create even if matches exist)
|
|
218
|
-
const shouldShow = isCreatable && !!columnDef.onCreateNew;
|
|
219
|
-
|
|
220
|
-
setShowCreateOption(shouldShow);
|
|
221
|
-
} else {
|
|
222
|
-
setShowCreateOption(false);
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// Check initial value in case user has already typed
|
|
227
|
-
const initialValue = searchInput.value;
|
|
228
|
-
if (initialValue) {
|
|
229
|
-
const currentSearch = initialValue;
|
|
230
|
-
setSearchTerm(currentSearch);
|
|
231
|
-
|
|
232
|
-
// Check if search doesn't match any option (including items in groups)
|
|
233
|
-
if (currentSearch.trim()) {
|
|
234
|
-
const searchLower = currentSearch.toLowerCase().trim();
|
|
235
|
-
|
|
236
|
-
type FieldOption =
|
|
237
|
-
| { value: string | number; label: string }
|
|
238
|
-
| { type: 'group'; label: string; items: Array<{ value: string | number; label: string }> }
|
|
239
|
-
| { type: 'separator' };
|
|
240
|
-
|
|
241
|
-
const checkMatch = (opt: FieldOption): boolean => {
|
|
242
|
-
if ('value' in opt && !('type' in opt)) {
|
|
243
|
-
return opt.label.toLowerCase().includes(searchLower);
|
|
244
|
-
}
|
|
245
|
-
if ('type' in opt && opt.type === 'group') {
|
|
246
|
-
return (opt as { type: 'group'; label: string; items: Array<{ value: string | number; label: string }> }).items.some((item: { value: string | number; label: string }) => item.label.toLowerCase().includes(searchLower));
|
|
247
|
-
}
|
|
248
|
-
return false;
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const hasMatch = (columnDef.fieldOptions || []).some(checkMatch);
|
|
252
|
-
// Always show create option when there's a search term (user might want to create even if matches exist)
|
|
253
|
-
const shouldShow = isCreatable && !!columnDef.onCreateNew;
|
|
254
|
-
setShowCreateOption(shouldShow);
|
|
255
|
-
} else {
|
|
256
|
-
setShowCreateOption(false);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
searchInput.addEventListener('input', handleInput);
|
|
261
|
-
|
|
262
|
-
return () => {
|
|
263
|
-
searchInput?.removeEventListener('input', handleInput);
|
|
264
|
-
};
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
// Try to find immediately
|
|
268
|
-
let cleanup: (() => void) | null = findAndAttachSearchInput();
|
|
269
|
-
|
|
270
|
-
// If not found, try again after a short delay (SelectContent might render asynchronously)
|
|
271
|
-
if (!cleanup) {
|
|
272
|
-
let timeoutCleanup: (() => void) | null = null;
|
|
273
|
-
const timeoutId = setTimeout(() => {
|
|
274
|
-
timeoutCleanup = findAndAttachSearchInput();
|
|
275
|
-
}, 50);
|
|
276
|
-
|
|
277
|
-
return () => {
|
|
278
|
-
clearTimeout(timeoutId);
|
|
279
|
-
timeoutCleanup?.();
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return cleanup;
|
|
284
|
-
}, [isOpen, isSearchable, isCreatable, columnDef.fieldOptions, columnDef.onCreateNew]);
|
|
285
|
-
|
|
286
|
-
const handleCreateNew = React.useCallback(async () => {
|
|
287
|
-
if (!isCreatable || !columnDef.onCreateNew || !searchTerm.trim()) return;
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const newValue = await columnDef.onCreateNew(searchTerm.trim());
|
|
291
|
-
onChange(newValue);
|
|
292
|
-
setSearchTerm('');
|
|
293
|
-
setShowCreateOption(false);
|
|
294
|
-
} catch (error) {
|
|
295
|
-
logger.error('Error creating new item:', error);
|
|
296
|
-
}
|
|
297
|
-
}, [isCreatable, columnDef.onCreateNew, searchTerm, onChange, logger]);
|
|
298
|
-
|
|
299
|
-
return (
|
|
300
|
-
<Select
|
|
301
|
-
ref={selectRef}
|
|
302
|
-
value={String(currentValue)}
|
|
303
|
-
onValueChange={(newValue) => {
|
|
304
|
-
if (newValue.startsWith('__create_new__')) {
|
|
305
|
-
handleCreateNew();
|
|
306
|
-
} else {
|
|
307
|
-
onChange(newValue as CellValue);
|
|
308
|
-
}
|
|
309
|
-
}}
|
|
310
|
-
onOpenChange={(open) => {
|
|
311
|
-
setIsOpen(open);
|
|
312
|
-
if (!open) {
|
|
313
|
-
setSearchTerm('');
|
|
314
|
-
setShowCreateOption(false);
|
|
315
|
-
}
|
|
316
|
-
}}
|
|
317
|
-
>
|
|
318
|
-
<SelectTrigger className="h-8">
|
|
319
|
-
<SelectValue placeholder={placeholder || `Select ${columnDef.header || 'option'}...`} />
|
|
320
|
-
</SelectTrigger>
|
|
321
|
-
<SelectContent
|
|
322
|
-
searchable={Boolean(isSearchable)}
|
|
323
|
-
searchPlaceholder={`Search ${columnDef.header || 'options'}...`}
|
|
324
|
-
maxHeight={columnDef.selectMaxHeight}
|
|
325
|
-
className={columnDef.selectContentClassName}
|
|
326
|
-
style={columnDef.selectContentStyle}
|
|
327
|
-
>
|
|
328
|
-
{columnDef.fieldOptions?.map((option, index) => {
|
|
329
|
-
// Simple option item
|
|
330
|
-
if ('value' in option && !('type' in option)) {
|
|
331
|
-
return (
|
|
332
|
-
<SelectItem key={`${option.value}-${index}`} value={String(option.value)}>
|
|
333
|
-
{option.label}
|
|
334
|
-
</SelectItem>
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Separator
|
|
339
|
-
if ('type' in option && option.type === 'separator') {
|
|
340
|
-
return <SelectSeparator key={`separator-${index}`} />;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Group with label
|
|
344
|
-
if ('type' in option && option.type === 'group') {
|
|
345
|
-
const groupOption = option as { type: 'group'; label: string; items: Array<{ value: string | number; label: string }> };
|
|
346
|
-
return (
|
|
347
|
-
<SelectGroup key={`group-${groupOption.label}-${index}`}>
|
|
348
|
-
<SelectLabel>{groupOption.label}</SelectLabel>
|
|
349
|
-
{groupOption.items.map((item: { value: string | number; label: string }) => (
|
|
350
|
-
<SelectItem key={`${item.value}-${index}`} value={String(item.value)}>
|
|
351
|
-
{item.label}
|
|
352
|
-
</SelectItem>
|
|
353
|
-
))}
|
|
354
|
-
</SelectGroup>
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return null;
|
|
359
|
-
})}
|
|
360
|
-
{showCreateOption && isCreatable && searchTerm.trim() && columnDef.onCreateNew && (
|
|
361
|
-
<SelectItem
|
|
362
|
-
key="__create_new__"
|
|
363
|
-
value={`__create_new__${searchTerm}`}
|
|
364
|
-
className="bg-main-100 font-medium border-t border-main-200"
|
|
365
|
-
>
|
|
366
|
-
Create "{searchTerm}"
|
|
367
|
-
</SelectItem>
|
|
368
|
-
)}
|
|
369
|
-
</SelectContent>
|
|
370
|
-
</Select>
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Helper function to render the appropriate input type based on column configuration
|
|
375
|
-
const renderEditField = <TData extends DataRecord>(
|
|
376
|
-
column: Column<TData, unknown>,
|
|
377
|
-
value: CellValue,
|
|
378
|
-
onChange: (value: CellValue | Record<string, CellValue>) => void,
|
|
379
|
-
editingData: Record<string, CellValue> = {},
|
|
380
|
-
placeholder?: string
|
|
381
|
-
) => {
|
|
382
|
-
const columnDef = column.columnDef as EditableColumnDef<TData>;
|
|
383
|
-
|
|
384
|
-
// Check if column is editable (default: true)
|
|
385
|
-
if (columnDef.editable === false) {
|
|
386
|
-
// Return the original value as text if column is not editable
|
|
387
|
-
return <span className="text-sm text-sec-600">{String(value ?? '')}</span>;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Check for custom field type
|
|
391
|
-
if (columnDef.fieldType === 'select' && columnDef.fieldOptions) {
|
|
392
|
-
// Use editAccessorKey if specified, otherwise use the column id
|
|
393
|
-
const accessorKey = columnDef.editAccessorKey || column.id;
|
|
394
|
-
const currentValue = editingData[accessorKey] ?? value ?? '';
|
|
395
|
-
|
|
396
|
-
return (
|
|
397
|
-
<SelectEditField
|
|
398
|
-
columnDef={columnDef}
|
|
399
|
-
accessorKey={accessorKey}
|
|
400
|
-
currentValue={currentValue}
|
|
401
|
-
placeholder={placeholder}
|
|
402
|
-
onChange={(newValue) => onChange({ [accessorKey]: newValue })}
|
|
403
|
-
/>
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Check for number type (applies to number, currency, and percentage fields)
|
|
408
|
-
if (columnDef.fieldType === 'number') {
|
|
409
|
-
// Hide spinner arrows by default for all number-related fields
|
|
410
|
-
// Currency and percentage columns use fieldType: 'number' with formatting in cell renderer
|
|
411
|
-
// Only show spinners if explicitly set to false
|
|
412
|
-
const hideSpinners = columnDef.hideNumberSpinners !== false; // Default to true
|
|
413
|
-
return (
|
|
414
|
-
<Input
|
|
415
|
-
type="number"
|
|
416
|
-
value={String(value ?? '')}
|
|
417
|
-
onChange={(e) => onChange(e.target.value as unknown as CellValue)}
|
|
418
|
-
placeholder={placeholder || `Enter ${columnDef.header || column.id}...`}
|
|
419
|
-
className={`h-8 ${hideSpinners ? 'datatable-number-no-spinners' : ''}`}
|
|
420
|
-
/>
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Check for date type
|
|
425
|
-
if (columnDef.fieldType === 'date') {
|
|
426
|
-
return (
|
|
427
|
-
<Input
|
|
428
|
-
type="date"
|
|
429
|
-
value={String(value ?? '')}
|
|
430
|
-
onChange={(e) => onChange(e.target.value as unknown as CellValue)}
|
|
431
|
-
className="h-8"
|
|
432
|
-
/>
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Default to text input
|
|
437
|
-
return (
|
|
438
|
-
<Input
|
|
439
|
-
type="text"
|
|
440
|
-
value={String(value ?? '')}
|
|
441
|
-
onChange={(e) => onChange(e.target.value as unknown as CellValue)}
|
|
442
|
-
placeholder={placeholder || `Enter ${columnDef.header || column.id}...`}
|
|
443
|
-
className="h-8"
|
|
444
|
-
/>
|
|
445
|
-
);
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
// Row component props interface
|
|
449
|
-
interface RowProps {
|
|
450
|
-
row: any;
|
|
451
|
-
style?: React.CSSProperties;
|
|
452
|
-
isEditing?: boolean;
|
|
453
|
-
editingData?: Record<string, any>;
|
|
454
|
-
onEditingDataChange?: (data: Record<string, any>) => void;
|
|
455
|
-
onSaveEditing?: () => void;
|
|
456
|
-
onCancelEditing?: () => void;
|
|
457
|
-
getRowId?: (row: any, index: number) => string;
|
|
458
|
-
grouping: string[];
|
|
459
|
-
editingRowId?: string | null;
|
|
460
|
-
hierarchical?: HierarchicalConfig & {
|
|
461
|
-
state?: {
|
|
462
|
-
isExpanded: (rowId: string) => boolean;
|
|
463
|
-
hasChildren: (rowId: string) => boolean;
|
|
464
|
-
getChildrenCount: (rowId: string) => number;
|
|
465
|
-
toggleRow: (rowId: string) => void;
|
|
466
|
-
};
|
|
467
|
-
};
|
|
468
|
-
actions?: Array<{
|
|
469
|
-
label: string;
|
|
470
|
-
onClick: (row: any) => void;
|
|
471
|
-
icon?: any;
|
|
472
|
-
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost';
|
|
473
|
-
disabled?: boolean | ((row?: any) => boolean);
|
|
474
|
-
visible?: boolean | ((row?: any) => boolean);
|
|
475
|
-
testId?: string;
|
|
476
|
-
showInEditMode?: boolean;
|
|
477
|
-
hideInViewMode?: boolean;
|
|
478
|
-
showInViewMode?: boolean;
|
|
479
|
-
hidden?: boolean;
|
|
480
|
-
showForParent?: boolean;
|
|
481
|
-
showForChild?: boolean;
|
|
482
|
-
parentIcon?: any;
|
|
483
|
-
childIcon?: any;
|
|
484
|
-
parentLabel?: string;
|
|
485
|
-
childLabel?: string;
|
|
486
|
-
}>;
|
|
487
|
-
rbac?: {
|
|
488
|
-
pageId?: string;
|
|
489
|
-
pageName?: string;
|
|
490
|
-
};
|
|
491
|
-
permissions?: {
|
|
492
|
-
canRead: { can: boolean; isLoading: boolean };
|
|
493
|
-
canCreate: { can: boolean; isLoading: boolean };
|
|
494
|
-
canUpdate: { can: boolean; isLoading: boolean };
|
|
495
|
-
canDelete: { can: boolean; isLoading: boolean };
|
|
496
|
-
canExport: { can: boolean; isLoading: boolean };
|
|
497
|
-
canImport: { can: boolean; isLoading: boolean };
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Row component with proper memoization
|
|
502
|
-
const RowComponent = React.memo(({
|
|
503
|
-
row,
|
|
504
|
-
style,
|
|
505
|
-
isEditing,
|
|
506
|
-
editingData,
|
|
507
|
-
onEditingDataChange,
|
|
508
|
-
onSaveEditing,
|
|
509
|
-
onCancelEditing,
|
|
510
|
-
getRowId,
|
|
511
|
-
grouping,
|
|
512
|
-
editingRowId,
|
|
513
|
-
hierarchical,
|
|
514
|
-
actions,
|
|
515
|
-
rbac,
|
|
516
|
-
permissions
|
|
517
|
-
}: RowProps) => {
|
|
518
|
-
const rowRef = useRef<HTMLTableRowElement>(null);
|
|
519
|
-
const firstInputRef = useRef<HTMLInputElement>(null);
|
|
520
|
-
const logger = createLogger('RowComponent');
|
|
521
|
-
|
|
522
|
-
const rowId = getRowIdSafe(row.original, row.index, getRowId);
|
|
523
|
-
|
|
524
|
-
// Hierarchical row styling - moved to top to avoid hoisting issues
|
|
525
|
-
const hierarchicalRow = row.original as HierarchicalDataRow;
|
|
526
|
-
const isHierarchical = hierarchical?.enabled && hierarchicalRow?.isParent !== undefined;
|
|
527
|
-
const isParent = isHierarchical && hierarchicalRow.isParent;
|
|
528
|
-
const isChild = isHierarchical && !hierarchicalRow.isParent;
|
|
529
|
-
|
|
530
|
-
// React Compiler handles memoization automatically
|
|
531
|
-
const visibleCells = row.getVisibleCells();
|
|
532
|
-
const isSelected = typeof row.getIsSelected === 'function' ? row.getIsSelected() : false;
|
|
533
|
-
|
|
534
|
-
// Auto-focus first editable field when entering edit mode
|
|
535
|
-
useEffect(() => {
|
|
536
|
-
if (isEditing && firstInputRef.current) {
|
|
537
|
-
firstInputRef.current.focus();
|
|
538
|
-
firstInputRef.current.select();
|
|
539
|
-
}
|
|
540
|
-
}, [isEditing]);
|
|
541
|
-
|
|
542
|
-
// Keyboard navigation (Enter to save, Escape to cancel)
|
|
543
|
-
useEffect(() => {
|
|
544
|
-
if (!isEditing) return;
|
|
545
|
-
|
|
546
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
547
|
-
const target = event.target as HTMLElement;
|
|
548
|
-
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
|
549
|
-
if (event.key === 'Enter' && !event.shiftKey && target.tagName === 'INPUT') {
|
|
550
|
-
event.preventDefault();
|
|
551
|
-
onSaveEditing?.();
|
|
552
|
-
} else if (event.key === 'Escape') {
|
|
553
|
-
event.preventDefault();
|
|
554
|
-
onCancelEditing?.();
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
const currentRow = rowRef.current;
|
|
560
|
-
if (currentRow) {
|
|
561
|
-
currentRow.addEventListener('keydown', handleKeyDown);
|
|
562
|
-
return () => {
|
|
563
|
-
currentRow.removeEventListener('keydown', handleKeyDown);
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
}, [isEditing, onSaveEditing, onCancelEditing]);
|
|
567
|
-
|
|
568
|
-
// Handle grouped rows
|
|
569
|
-
if (row.getIsGrouped && row.getIsGrouped()) {
|
|
570
|
-
const groupValue = row.getValue(grouping[0]);
|
|
571
|
-
const subRowsCount = row.subRows?.length || 0;
|
|
572
|
-
const isExpanded = row.getIsExpanded();
|
|
573
|
-
|
|
574
|
-
// Get child rows for aggregation (convert TanStack Row to original data)
|
|
575
|
-
const childRows: DataRecord[] = row.subRows?.map((subRow: any) => subRow.original) || [];
|
|
576
|
-
|
|
577
|
-
// Render individual cells for each column with aggregation support
|
|
578
|
-
return (
|
|
579
|
-
<tr className="bg-sec-50 hover:bg-sec-100" style={style}>
|
|
580
|
-
{visibleCells.map((cell: any, cellIndex: number) => {
|
|
581
|
-
const columnDef = cell.column.columnDef as DataTableColumn<DataRecord>;
|
|
582
|
-
const isGroupingColumn = cellIndex === 0 || grouping.includes(cell.column.id || '');
|
|
583
|
-
|
|
584
|
-
// For the grouping column, show the group header with expand/collapse
|
|
585
|
-
if (isGroupingColumn && cellIndex === 0) {
|
|
586
|
-
return (
|
|
587
|
-
<td
|
|
588
|
-
key={cell.id}
|
|
589
|
-
className={getTableCellClasses({
|
|
590
|
-
isCompact: true,
|
|
591
|
-
className: "px-3 py-2 flex items-center font-medium"
|
|
592
|
-
})}
|
|
593
|
-
>
|
|
594
|
-
<Button
|
|
595
|
-
variant="ghost"
|
|
596
|
-
size="sm"
|
|
597
|
-
onClick={() => row.toggleExpanded()}
|
|
598
|
-
className="p-0 h-auto mr-2"
|
|
599
|
-
>
|
|
600
|
-
{isExpanded ? (
|
|
601
|
-
<ChevronDown className="size-4" />
|
|
602
|
-
) : (
|
|
603
|
-
<ChevronRight className="size-4" />
|
|
604
|
-
)}
|
|
605
|
-
</Button>
|
|
606
|
-
<span className="text-sm">
|
|
607
|
-
{String(groupValue)} ({subRowsCount} items)
|
|
608
|
-
</span>
|
|
609
|
-
</td>
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// For other columns, check if they have aggregation functions
|
|
614
|
-
if (columnDef.aggregateFn && childRows.length > 0) {
|
|
615
|
-
try {
|
|
616
|
-
// Call the aggregation function with child rows
|
|
617
|
-
const aggregatedValue = columnDef.aggregateFn(childRows, columnDef);
|
|
618
|
-
|
|
619
|
-
// Use aggregateCell renderer if provided, otherwise use regular cell renderer
|
|
620
|
-
let cellContent: React.ReactNode;
|
|
621
|
-
if (columnDef.aggregateCell) {
|
|
622
|
-
cellContent = columnDef.aggregateCell(aggregatedValue, childRows, columnDef);
|
|
623
|
-
} else if (columnDef.cell) {
|
|
624
|
-
// Use regular cell renderer with a mock cell context
|
|
625
|
-
// Create a mock cell object for the aggregated value
|
|
626
|
-
const mockCell = {
|
|
627
|
-
...cell,
|
|
628
|
-
getValue: () => aggregatedValue,
|
|
629
|
-
renderValue: () => aggregatedValue,
|
|
630
|
-
};
|
|
631
|
-
cellContent = flexRender(columnDef.cell, {
|
|
632
|
-
...mockCell,
|
|
633
|
-
row: row,
|
|
634
|
-
column: cell.column,
|
|
635
|
-
cell: mockCell,
|
|
636
|
-
getValue: () => aggregatedValue,
|
|
637
|
-
renderValue: () => aggregatedValue,
|
|
638
|
-
});
|
|
639
|
-
} else {
|
|
640
|
-
// Fallback to displaying the raw value
|
|
641
|
-
cellContent = aggregatedValue != null ? String(aggregatedValue) : '';
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
return (
|
|
645
|
-
<td
|
|
646
|
-
key={cell.id}
|
|
647
|
-
className={getTableCellClasses({
|
|
648
|
-
isCompact: true,
|
|
649
|
-
className: `px-3 py-2 ${cell.column.columnDef.meta?.align === 'right' ? 'text-right' : ''}`
|
|
650
|
-
})}
|
|
651
|
-
>
|
|
652
|
-
{cellContent}
|
|
653
|
-
</td>
|
|
654
|
-
);
|
|
655
|
-
} catch (error) {
|
|
656
|
-
// If aggregation fails, show empty cell
|
|
657
|
-
logger.warn('Error in aggregation function:', error);
|
|
658
|
-
return (
|
|
659
|
-
<td
|
|
660
|
-
key={cell.id}
|
|
661
|
-
className={getTableCellClasses({
|
|
662
|
-
isCompact: true,
|
|
663
|
-
className: "px-3 py-2"
|
|
664
|
-
})}
|
|
665
|
-
/>
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// For columns without aggregation, show empty cell
|
|
671
|
-
return (
|
|
672
|
-
<td
|
|
673
|
-
key={cell.id}
|
|
674
|
-
className={getTableCellClasses({
|
|
675
|
-
isCompact: true,
|
|
676
|
-
className: "px-3 py-2"
|
|
677
|
-
})}
|
|
678
|
-
/>
|
|
679
|
-
);
|
|
680
|
-
})}
|
|
681
|
-
</tr>
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// CRITICAL FIX: Removed the buggy filter that was hiding all rows when grouping is enabled
|
|
686
|
-
// TanStack Table's getRowModel() already correctly handles collapsing/expanding groups,
|
|
687
|
-
// returning only visible rows (group headers + children of expanded groups).
|
|
688
|
-
// The previous check was incorrectly filtering out all child rows regardless of parent expansion state,
|
|
689
|
-
// causing the bug where DataTable showed record count but no rows for large datasets.
|
|
690
|
-
//
|
|
691
|
-
// When grouping is enabled:
|
|
692
|
-
// - Group headers are rendered above (lines 317-426)
|
|
693
|
-
// - Child rows are ONLY in getRowModel().rows if their parent is expanded
|
|
694
|
-
// - Collapsed groups' children are NOT in getRowModel().rows (handled by TanStack Table)
|
|
695
|
-
//
|
|
696
|
-
// Therefore, we should trust getRowModel() and render all rows it returns.
|
|
697
|
-
|
|
698
|
-
// If we're in edit mode, use EditableRow for better UX (auto-focus, keyboard shortcuts)
|
|
699
|
-
if (isEditing && editingData && onEditingDataChange && onSaveEditing && onCancelEditing) {
|
|
700
|
-
return (
|
|
701
|
-
<EditableRow
|
|
702
|
-
row={row}
|
|
703
|
-
editingData={editingData}
|
|
704
|
-
onEditingDataChange={onEditingDataChange}
|
|
705
|
-
onSave={onSaveEditing}
|
|
706
|
-
onCancel={onCancelEditing}
|
|
707
|
-
actions={actions ?? []}
|
|
708
|
-
getRowId={getRowId}
|
|
709
|
-
isParent={isParent}
|
|
710
|
-
hierarchical={!!hierarchical}
|
|
711
|
-
/>
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Regular row (not in edit mode)
|
|
716
|
-
// visibleCells is already memoized above
|
|
717
|
-
|
|
718
|
-
// Calculate indentation for child rows
|
|
719
|
-
const indentSize = hierarchical?.indentSize || 24;
|
|
720
|
-
const indentation = React.useMemo(() => {
|
|
721
|
-
return isChild && hierarchical?.state ?
|
|
722
|
-
calculateIndentation(hierarchicalRow, [], indentSize) : 0;
|
|
723
|
-
}, [isChild, hierarchical?.state, hierarchicalRow, indentSize]);
|
|
724
|
-
|
|
725
|
-
// Apply hierarchical row classes - combine with standard row classes
|
|
726
|
-
const rowClassName = React.useMemo(() => {
|
|
727
|
-
if (isHierarchical) {
|
|
728
|
-
const hierarchicalClass = isParent
|
|
729
|
-
? hierarchical?.parentRowClassName || 'bg-main-50 hover:bg-main-100 font-medium'
|
|
730
|
-
: hierarchical?.childRowClassName || 'bg-sec-25 hover:bg-sec-50';
|
|
731
|
-
// Combine with standard row classes for consistent hover behavior
|
|
732
|
-
return cn(
|
|
733
|
-
getTableRowClasses({ isSelected, isVirtualized: false }),
|
|
734
|
-
hierarchicalClass
|
|
735
|
-
);
|
|
736
|
-
}
|
|
737
|
-
// Use standard row classes when not hierarchical
|
|
738
|
-
return getTableRowClasses({ isSelected, isVirtualized: false });
|
|
739
|
-
}, [isHierarchical, isParent, isSelected, hierarchical]);
|
|
740
|
-
|
|
741
|
-
return (
|
|
742
|
-
<tr
|
|
743
|
-
ref={rowRef}
|
|
744
|
-
key={row.id}
|
|
745
|
-
role="row"
|
|
746
|
-
style={{
|
|
747
|
-
...style,
|
|
748
|
-
...(isChild && indentation > 0 ? { paddingLeft: `${indentation}px` } : {})
|
|
749
|
-
}}
|
|
750
|
-
className={rowClassName}
|
|
751
|
-
aria-selected={isSelected ? 'true' : 'false'}
|
|
752
|
-
>
|
|
753
|
-
{visibleCells.map((cell: any, cellIndex: number) => {
|
|
754
|
-
const isFirstCell = cellIndex === 0;
|
|
755
|
-
const shouldShowExpansionButton = isHierarchical && isParent && isFirstCell && hierarchical?.state;
|
|
756
|
-
const isExpanded = shouldShowExpansionButton ? hierarchical?.state?.isExpanded(rowId) : false;
|
|
757
|
-
const hasChildren = shouldShowExpansionButton ? hierarchical?.state?.hasChildren(rowId) : false;
|
|
758
|
-
|
|
759
|
-
return (
|
|
760
|
-
<td
|
|
761
|
-
key={cell.id}
|
|
762
|
-
className={getTableCellClasses({
|
|
763
|
-
isCompact: true,
|
|
764
|
-
className: `px-3 py-2 ${cell.column.id === 'actions' ? 'whitespace-nowrap' : 'whitespace-normal break-words'} ${cell.column.columnDef.meta?.align === 'right' ? 'text-right' : ''}`
|
|
765
|
-
})}
|
|
766
|
-
>
|
|
767
|
-
{shouldShowExpansionButton && hasChildren && (
|
|
768
|
-
<Button
|
|
769
|
-
variant="ghost"
|
|
770
|
-
size="sm"
|
|
771
|
-
onClick={() => hierarchical?.state?.toggleRow(rowId)}
|
|
772
|
-
className="size-6 p-0 flex-shrink-0"
|
|
773
|
-
aria-label={isExpanded ? 'Collapse row' : 'Expand row'}
|
|
774
|
-
title={isExpanded ? 'Collapse row' : 'Expand row'}
|
|
775
|
-
>
|
|
776
|
-
{isExpanded ? (
|
|
777
|
-
<ChevronDown className="size-4" />
|
|
778
|
-
) : (
|
|
779
|
-
<ChevronRight className="size-4" />
|
|
780
|
-
)}
|
|
781
|
-
</Button>
|
|
782
|
-
)}
|
|
783
|
-
{cell.column.id === 'actions' ? (
|
|
784
|
-
<ActionButtons
|
|
785
|
-
row={row}
|
|
786
|
-
actions={actions}
|
|
787
|
-
isEditing={isEditing}
|
|
788
|
-
isParent={isParent}
|
|
789
|
-
hierarchical={!!hierarchical}
|
|
790
|
-
rbac={rbac}
|
|
791
|
-
permissions={permissions}
|
|
792
|
-
/>
|
|
793
|
-
) : (
|
|
794
|
-
flexRender(cell.column.columnDef.cell, {
|
|
795
|
-
...cell.getContext(),
|
|
796
|
-
hierarchical: hierarchical,
|
|
797
|
-
isParent: isParent,
|
|
798
|
-
isChild: isChild,
|
|
799
|
-
isHierarchical: isHierarchical,
|
|
800
|
-
rowId: rowId,
|
|
801
|
-
isExpanded: isExpanded,
|
|
802
|
-
hasChildren: hasChildren,
|
|
803
|
-
})
|
|
804
|
-
)}
|
|
805
|
-
</td>
|
|
806
|
-
);
|
|
807
|
-
})}
|
|
808
|
-
</tr>
|
|
809
|
-
);
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
RowComponent.displayName = 'RowComponent';
|
|
813
|
-
|
|
814
|
-
// Custom comparison function for React.memo to prevent unnecessary re-renders
|
|
815
|
-
// This compares the actual row data and props, not just object references
|
|
816
|
-
const areRowPropsEqual = (prevProps: RowProps, nextProps: RowProps): boolean => {
|
|
817
|
-
// Compare row by ID and index (stable identifiers)
|
|
818
|
-
if (prevProps.row.id !== nextProps.row.id || prevProps.row.index !== nextProps.row.index) {
|
|
819
|
-
return false;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Compare editing state
|
|
823
|
-
if (prevProps.isEditing !== nextProps.isEditing) {
|
|
824
|
-
return false;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if (prevProps.editingRowId !== nextProps.editingRowId) {
|
|
828
|
-
return false;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Compare style object (shallow comparison)
|
|
832
|
-
if (prevProps.style !== nextProps.style) {
|
|
833
|
-
if (!prevProps.style || !nextProps.style) {
|
|
834
|
-
return false;
|
|
835
|
-
}
|
|
836
|
-
// Convert to records for comparison
|
|
837
|
-
const prevStyle = prevProps.style as Record<string, unknown>;
|
|
838
|
-
const nextStyle = nextProps.style as Record<string, unknown>;
|
|
839
|
-
const styleKeys = new Set([...Object.keys(prevStyle), ...Object.keys(nextStyle)]);
|
|
840
|
-
for (const key of styleKeys) {
|
|
841
|
-
if (prevStyle[key] !== nextStyle[key]) {
|
|
842
|
-
return false;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Compare other primitive props
|
|
848
|
-
if (prevProps.grouping.length !== nextProps.grouping.length ||
|
|
849
|
-
prevProps.grouping.some((id, i) => id !== nextProps.grouping[i])) {
|
|
850
|
-
return false;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// For hierarchical state, compare the enabled flag and state functions
|
|
854
|
-
if (prevProps.hierarchical?.enabled !== nextProps.hierarchical?.enabled) {
|
|
855
|
-
return false;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Compare row selection state by checking the function result
|
|
859
|
-
const prevSelected = typeof prevProps.row.getIsSelected === 'function' ? prevProps.row.getIsSelected() : false;
|
|
860
|
-
const nextSelected = typeof nextProps.row.getIsSelected === 'function' ? nextProps.row.getIsSelected() : false;
|
|
861
|
-
if (prevSelected !== nextSelected) {
|
|
862
|
-
return false;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// If all checks pass, props are equal
|
|
866
|
-
return true;
|
|
867
|
-
};
|
|
868
|
-
|
|
869
|
-
// Use the memoized RowComponent with custom comparison
|
|
870
|
-
const MemoizedRow = React.memo(RowComponent, areRowPropsEqual);
|
|
871
|
-
|
|
872
116
|
/**
|
|
873
117
|
* Unified table body component with intelligent virtualization
|
|
874
118
|
*/
|
|
@@ -885,7 +129,7 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
885
129
|
onSaveEditing,
|
|
886
130
|
onCancelEditing,
|
|
887
131
|
grouping,
|
|
888
|
-
aggregates,
|
|
132
|
+
aggregates: _aggregates,
|
|
889
133
|
getRowId,
|
|
890
134
|
emptyState,
|
|
891
135
|
isFiltered,
|
|
@@ -893,33 +137,26 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
893
137
|
enableFiltering = false,
|
|
894
138
|
showFilterRow = false,
|
|
895
139
|
dataLength,
|
|
896
|
-
virtualHeight = 600,
|
|
140
|
+
virtualHeight: _virtualHeight = 600,
|
|
897
141
|
forceVirtualization = false,
|
|
898
142
|
hierarchical,
|
|
899
143
|
actions = [],
|
|
900
144
|
rbac,
|
|
901
|
-
permissions
|
|
145
|
+
permissions,
|
|
902
146
|
}: UnifiedTableBodyProps<TData>) {
|
|
903
147
|
const logger = createLogger('UnifiedTableBody');
|
|
904
|
-
|
|
905
|
-
const headerRef = useRef<HTMLTableSectionElement>(null);
|
|
148
|
+
|
|
906
149
|
const bodyRef = useRef<HTMLTableSectionElement>(null);
|
|
907
150
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
908
151
|
|
|
909
|
-
// Determine if virtualization should be used
|
|
910
152
|
const shouldVirtualize = forceVirtualization || dataLength > VIRTUALIZATION_THRESHOLD;
|
|
911
|
-
|
|
912
|
-
// Get table data
|
|
153
|
+
|
|
913
154
|
const rows = table.getRowModel().rows;
|
|
914
155
|
const headerGroups = table.getHeaderGroups();
|
|
915
156
|
|
|
916
|
-
// CRITICAL FIX: Virtual scrolling requires a scroll container (parentRef).
|
|
917
|
-
// If virtualization is enabled but no scroll container exists, fall back to standard rendering.
|
|
918
|
-
// This fixes the bug where rows exist but nothing renders when virtualization is enabled.
|
|
919
157
|
const hasScrollContainer = !!parentRef.current;
|
|
920
158
|
const effectiveShouldVirtualize = shouldVirtualize && hasScrollContainer;
|
|
921
|
-
|
|
922
|
-
// Virtual scrolling setup - only create virtualizer when we have a scroll container
|
|
159
|
+
|
|
923
160
|
const virtualizer = useVirtualizer({
|
|
924
161
|
count: effectiveShouldVirtualize ? rows.length : 0,
|
|
925
162
|
getScrollElement: () => parentRef.current || null,
|
|
@@ -928,9 +165,7 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
928
165
|
});
|
|
929
166
|
|
|
930
167
|
const virtualRows = effectiveShouldVirtualize ? virtualizer.getVirtualItems() : [];
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
// Warning if virtualization is expected but no container exists
|
|
168
|
+
|
|
934
169
|
useEffect(() => {
|
|
935
170
|
if (shouldVirtualize && !hasScrollContainer) {
|
|
936
171
|
logger.warn('Virtualization enabled but no scroll container found. Falling back to standard rendering.', {
|
|
@@ -940,17 +175,11 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
940
175
|
}
|
|
941
176
|
}, [shouldVirtualize, hasScrollContainer, rows.length, dataLength, logger]);
|
|
942
177
|
|
|
943
|
-
|
|
944
|
-
// Render table content
|
|
945
178
|
const renderTableContent = () => {
|
|
946
179
|
if (rows.length === 0) {
|
|
947
180
|
return (
|
|
948
181
|
<tr>
|
|
949
|
-
<td
|
|
950
|
-
colSpan={table.getVisibleFlatColumns().length}
|
|
951
|
-
className="px-3 py-2"
|
|
952
|
-
role="status"
|
|
953
|
-
>
|
|
182
|
+
<td colSpan={table.getVisibleFlatColumns().length} className="px-3 py-2" role="status">
|
|
954
183
|
<EmptyState
|
|
955
184
|
title={emptyState?.title}
|
|
956
185
|
description={emptyState?.description}
|
|
@@ -965,14 +194,13 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
965
194
|
}
|
|
966
195
|
|
|
967
196
|
if (effectiveShouldVirtualize && virtualRows.length > 0) {
|
|
968
|
-
|
|
969
|
-
return virtualRows.map((virtualRow) => {
|
|
197
|
+
return virtualRows.map((virtualRow: VirtualItem) => {
|
|
970
198
|
const row = rows[virtualRow.index];
|
|
971
199
|
if (!row) return null;
|
|
972
|
-
|
|
200
|
+
|
|
973
201
|
const rowId = getRowIdSafe(row.original, row.index, getRowId);
|
|
974
202
|
const isEditing = editingRowId === rowId;
|
|
975
|
-
|
|
203
|
+
|
|
976
204
|
return (
|
|
977
205
|
<MemoizedRow
|
|
978
206
|
key={row.id}
|
|
@@ -1000,32 +228,31 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
1000
228
|
/>
|
|
1001
229
|
);
|
|
1002
230
|
});
|
|
1003
|
-
} else {
|
|
1004
|
-
// Standard rendering
|
|
1005
|
-
return rows.map((row) => {
|
|
1006
|
-
const rowId = getRowIdSafe(row.original, row.index, getRowId);
|
|
1007
|
-
const isEditing = editingRowId === rowId;
|
|
1008
|
-
|
|
1009
|
-
return (
|
|
1010
|
-
<MemoizedRow
|
|
1011
|
-
key={row.id}
|
|
1012
|
-
row={row}
|
|
1013
|
-
isEditing={isEditing}
|
|
1014
|
-
editingData={editingData}
|
|
1015
|
-
onEditingDataChange={onEditingDataChange}
|
|
1016
|
-
onSaveEditing={onSaveEditing}
|
|
1017
|
-
onCancelEditing={onCancelEditing}
|
|
1018
|
-
getRowId={getRowId}
|
|
1019
|
-
grouping={grouping}
|
|
1020
|
-
editingRowId={editingRowId}
|
|
1021
|
-
hierarchical={hierarchical}
|
|
1022
|
-
actions={actions}
|
|
1023
|
-
rbac={rbac}
|
|
1024
|
-
permissions={permissions}
|
|
1025
|
-
/>
|
|
1026
|
-
);
|
|
1027
|
-
});
|
|
1028
231
|
}
|
|
232
|
+
|
|
233
|
+
return rows.map((row) => {
|
|
234
|
+
const rowId = getRowIdSafe(row.original, row.index, getRowId);
|
|
235
|
+
const isEditing = editingRowId === rowId;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<MemoizedRow
|
|
239
|
+
key={row.id}
|
|
240
|
+
row={row}
|
|
241
|
+
isEditing={isEditing}
|
|
242
|
+
editingData={editingData}
|
|
243
|
+
onEditingDataChange={onEditingDataChange}
|
|
244
|
+
onSaveEditing={onSaveEditing}
|
|
245
|
+
onCancelEditing={onCancelEditing}
|
|
246
|
+
getRowId={getRowId}
|
|
247
|
+
grouping={grouping}
|
|
248
|
+
editingRowId={editingRowId}
|
|
249
|
+
hierarchical={hierarchical}
|
|
250
|
+
actions={actions}
|
|
251
|
+
rbac={rbac}
|
|
252
|
+
permissions={permissions}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
});
|
|
1029
256
|
};
|
|
1030
257
|
|
|
1031
258
|
return (
|
|
@@ -1035,60 +262,50 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
1035
262
|
{isCreating && (
|
|
1036
263
|
<tr>
|
|
1037
264
|
{headerGroups[0]?.headers
|
|
1038
|
-
?.filter(header => {
|
|
1039
|
-
|
|
1040
|
-
return typeof header.column.getIsVisible === 'function'
|
|
1041
|
-
? header.column.getIsVisible()
|
|
1042
|
-
: true;
|
|
265
|
+
?.filter((header) => {
|
|
266
|
+
return typeof header.column.getIsVisible === 'function' ? header.column.getIsVisible() : true;
|
|
1043
267
|
})
|
|
1044
|
-
?.filter(header => header.column.id !== 'actions')
|
|
268
|
+
?.filter((header) => header.column.id !== 'actions')
|
|
1045
269
|
?.map((header) => {
|
|
1046
|
-
// Handle select column separately - render empty checkbox cell for alignment
|
|
1047
270
|
if (header.column.id === 'select') {
|
|
1048
271
|
return (
|
|
1049
|
-
<td
|
|
1050
|
-
key={header.column.id}
|
|
272
|
+
<td
|
|
273
|
+
key={header.column.id}
|
|
1051
274
|
className={getTableCellClasses({
|
|
1052
275
|
isCompact: true,
|
|
1053
|
-
className:
|
|
276
|
+
className: 'px-3 py-2',
|
|
1054
277
|
})}
|
|
1055
278
|
>
|
|
1056
279
|
{/* Empty cell for selection checkbox to maintain alignment */}
|
|
1057
280
|
</td>
|
|
1058
281
|
);
|
|
1059
282
|
}
|
|
1060
|
-
|
|
1061
|
-
// Render edit fields for data columns
|
|
1062
|
-
// Determine the correct key to use for creationData
|
|
1063
|
-
// Priority: editAccessorKey > accessorKey > column.id
|
|
283
|
+
|
|
1064
284
|
const columnDef = header.column.columnDef as EditableColumnDef<TData>;
|
|
1065
285
|
const dataKey = columnDef.editAccessorKey || columnDef.accessorKey || header.column.id;
|
|
1066
|
-
|
|
1067
|
-
// Always render a cell to maintain alignment - renderEditField always returns something
|
|
286
|
+
|
|
1068
287
|
const editField = renderEditField(
|
|
1069
|
-
header.column,
|
|
1070
|
-
creationData[dataKey] ?? creationData[header.column.id] ?? '',
|
|
288
|
+
header.column,
|
|
289
|
+
creationData[dataKey] ?? creationData[header.column.id] ?? '',
|
|
1071
290
|
(value) => {
|
|
1072
291
|
if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
|
|
1073
292
|
onCreationDataChange({ ...creationData, ...(value as Record<string, CellValue>) });
|
|
1074
293
|
} else {
|
|
1075
|
-
// Use the determined dataKey for consistent data access
|
|
1076
294
|
onCreationDataChange({ ...creationData, [dataKey]: value as CellValue });
|
|
1077
295
|
}
|
|
1078
|
-
},
|
|
296
|
+
},
|
|
1079
297
|
creationData
|
|
1080
298
|
);
|
|
1081
|
-
|
|
299
|
+
|
|
1082
300
|
return (
|
|
1083
|
-
<td
|
|
1084
|
-
key={header.column.id}
|
|
301
|
+
<td
|
|
302
|
+
key={header.column.id}
|
|
1085
303
|
className={getTableCellClasses({
|
|
1086
304
|
isCompact: true,
|
|
1087
|
-
className:
|
|
305
|
+
className: 'px-3 py-2',
|
|
1088
306
|
})}
|
|
1089
307
|
>
|
|
1090
308
|
{editField || (
|
|
1091
|
-
// Fallback: render a text input if renderEditField somehow returns nothing
|
|
1092
309
|
<Input
|
|
1093
310
|
type="text"
|
|
1094
311
|
value={String(creationData[dataKey] ?? creationData[header.column.id] ?? '')}
|
|
@@ -1100,10 +317,10 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
1100
317
|
</td>
|
|
1101
318
|
);
|
|
1102
319
|
})}
|
|
1103
|
-
<td
|
|
320
|
+
<td
|
|
1104
321
|
className={getTableCellClasses({
|
|
1105
322
|
isCompact: true,
|
|
1106
|
-
className:
|
|
323
|
+
className: 'px-3 py-2 flex gap-1',
|
|
1107
324
|
})}
|
|
1108
325
|
>
|
|
1109
326
|
<button
|
|
@@ -1127,15 +344,10 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
1127
344
|
</td>
|
|
1128
345
|
</tr>
|
|
1129
346
|
)}
|
|
1130
|
-
|
|
347
|
+
|
|
1131
348
|
{/* Filter Row */}
|
|
1132
|
-
{showFilterRow && enableFiltering && (
|
|
1133
|
-
|
|
1134
|
-
table={table}
|
|
1135
|
-
visibleColumns={table.getHeaderGroups()[0]?.headers || []}
|
|
1136
|
-
/>
|
|
1137
|
-
)}
|
|
1138
|
-
|
|
349
|
+
{showFilterRow && enableFiltering && <FilterRow table={table} visibleColumns={table.getHeaderGroups()[0]?.headers || []} />}
|
|
350
|
+
|
|
1139
351
|
{/* Table Content */}
|
|
1140
352
|
{renderTableContent()}
|
|
1141
353
|
</tbody>
|