@jmruthers/pace-core 0.5.136 → 0.5.139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-CYOHOX3O.js → DataTable-JXFCA2BJ.js} +10 -9
- package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
- package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
- package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
- package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
- package/dist/chunk-4C7EXCAR.js.map +1 -0
- package/dist/{chunk-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
- package/dist/chunk-5JMOHWDI.js.map +1 -0
- package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
- package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
- package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
- package/dist/chunk-7QCC6MCP.js.map +1 -0
- package/dist/chunk-BJPBT3CU.js +21 -0
- package/dist/chunk-BJPBT3CU.js.map +1 -0
- package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
- package/dist/chunk-BOOI7GK2.js.map +1 -0
- package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
- package/dist/chunk-JISYG63F.js +70 -0
- package/dist/chunk-JISYG63F.js.map +1 -0
- package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
- package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
- package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
- package/dist/{chunk-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
- package/dist/chunk-TLT2ZR3L.js.map +1 -0
- package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
- package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +8 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -4
- package/dist/rbac/index.js +8 -9
- package/dist/schema-DTDZQe2u.d.ts +28 -0
- package/dist/types.d.ts +152 -3
- package/dist/types.js +51 -16
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +89 -4
- package/dist/utils.js +214 -96
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +1 -343
- package/dist/validation.js +3 -100
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +27 -0
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +84 -15
- package/docs/architecture/README.md +0 -1
- package/docs/styles/README.md +0 -2
- package/examples/RBAC/CompleteRBACExample.tsx +324 -0
- package/examples/RBAC/EventBasedApp.tsx +239 -0
- package/examples/RBAC/PermissionExample.tsx +151 -0
- package/examples/RBAC/index.ts +13 -0
- package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
- package/examples/public-pages/PublicEventPage.tsx +274 -0
- package/examples/public-pages/PublicPageApp.tsx +308 -0
- package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
- package/examples/public-pages/index.ts +14 -0
- package/package.json +1 -10
- package/src/__tests__/TEST_STANDARD.md +92 -0
- package/src/components/Badge/Badge.test.tsx +314 -0
- package/src/components/Badge/Badge.tsx +304 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
- package/src/components/DataTable/__tests__/styles.test.ts +1 -1
- package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
- package/src/components/DataTable/components/DataTableBody.tsx +461 -0
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
- package/src/components/DataTable/components/FilterRow.tsx +9 -3
- package/src/components/DataTable/components/PaginationControls.tsx +1 -0
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
- package/src/components/DataTable/core/ActionManager.ts +235 -0
- package/src/components/DataTable/core/ColumnManager.ts +205 -0
- package/src/components/DataTable/core/DataManager.ts +188 -0
- package/src/components/DataTable/core/DataTableContext.tsx +181 -0
- package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
- package/src/components/DataTable/core/PluginRegistry.ts +229 -0
- package/src/components/DataTable/core/StateManager.ts +311 -0
- package/src/components/DataTable/core/interfaces.ts +338 -0
- package/src/components/DataTable/styles.ts +27 -6
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/columnUtils.ts +40 -0
- package/src/components/DataTable/utils/debugTools.ts +609 -0
- package/src/components/DataTable/utils/index.ts +1 -0
- package/src/components/Dialog/README.md +804 -0
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
- package/src/components/Dialog/utils/safeHtml.ts +185 -0
- package/src/components/Footer/Footer.test.tsx +1 -1
- package/src/components/Form/Form.test.tsx +1 -1
- package/src/components/Form/FormErrorSummary.tsx +113 -0
- package/src/components/Form/FormFieldset.tsx +127 -0
- package/src/components/Form/FormLiveRegion.tsx +198 -0
- package/src/components/LoginForm/LoginForm.test.tsx +1 -1
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
- package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
- package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
- package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Select/Select.tsx +20 -8
- package/src/components/Table/__tests__/Table.test.tsx +1 -1
- package/src/components/index.ts +3 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
- package/src/index.ts +4 -0
- package/src/rbac/hooks/useCan.test.ts +24 -0
- package/src/rbac/hooks/usePermissions.ts +49 -12
- package/src/styles/core.css +3 -0
- package/src/utils/appConfig.ts +47 -0
- package/src/utils/appIdResolver.test.ts +499 -0
- package/src/utils/appIdResolver.ts +130 -0
- package/src/utils/appNameResolver.simple.test.ts +212 -0
- package/src/utils/appNameResolver.test.ts +121 -0
- package/src/utils/appNameResolver.ts +191 -0
- package/src/utils/audit.ts +127 -0
- package/src/utils/auth-utils.ts +96 -0
- package/src/utils/bundleAnalysis.ts +129 -0
- package/src/utils/cn.ts +7 -0
- package/src/utils/debugLogger.ts +67 -0
- package/src/utils/deviceFingerprint.ts +215 -0
- package/src/utils/dynamicUtils.ts +105 -0
- package/src/utils/file-reference.test.ts +788 -0
- package/src/utils/file-reference.ts +519 -0
- package/src/utils/formatDate.test.ts +237 -0
- package/src/utils/formatting.ts +133 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/lazyLoad.tsx +44 -0
- package/src/utils/logger.ts +179 -0
- package/src/utils/organisationContext.test.ts +322 -0
- package/src/utils/organisationContext.ts +153 -0
- package/src/utils/performanceBenchmark.ts +64 -0
- package/src/utils/performanceBudgets.ts +110 -0
- package/src/utils/permissionTypes.ts +37 -0
- package/src/utils/permissionUtils.test.ts +393 -0
- package/src/utils/permissionUtils.ts +34 -0
- package/src/utils/sanitization.ts +264 -0
- package/src/utils/schemaUtils.ts +37 -0
- package/src/utils/secureDataAccess.test.ts +711 -0
- package/src/utils/secureDataAccess.ts +377 -0
- package/src/utils/secureErrors.ts +79 -0
- package/src/utils/secureStorage.ts +244 -0
- package/src/utils/security.ts +156 -0
- package/src/utils/securityMonitor.ts +45 -0
- package/src/utils/sessionTracking.ts +126 -0
- package/src/utils/validation.ts +111 -0
- package/src/utils/validationUtils.ts +120 -0
- package/src/validation/index.ts +2 -2
- package/dist/chunk-444EZN6N.js.map +0 -1
- package/dist/chunk-APIBCTL2.js +0 -670
- package/dist/chunk-APIBCTL2.js.map +0 -1
- package/dist/chunk-HJGGOMQ6.js.map +0 -1
- package/dist/chunk-K2WWTH7O.js +0 -94
- package/dist/chunk-K2WWTH7O.js.map +0 -1
- package/dist/chunk-L6PGMCMD.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-NOHEVYVX.js.map +0 -1
- package/dist/chunk-TVYPTYOY.js.map +0 -1
- package/dist/validation-8npbysjg.d.ts +0 -177
- /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-JXFCA2BJ.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
- /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
- /package/dist/{chunk-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
- /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
- /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
- /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
- /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
- /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/index.ts +0 -0
- /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/index.ts +0 -0
- /package/examples/{components → components 2}/index.ts +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Virtualized DataTable Component
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/VirtualizedDataTable
|
|
5
|
+
* @since 0.3.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo, useRef, useCallback, memo, useLayoutEffect, useState } from 'react';
|
|
9
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
10
|
+
import { flexRender, type Table as TanStackTable } from '@tanstack/react-table';
|
|
11
|
+
import { cn } from '../../../utils/cn';
|
|
12
|
+
import type { DataRecord } from '../types';
|
|
13
|
+
|
|
14
|
+
export interface VirtualizedDataTableProps<TData extends DataRecord> {
|
|
15
|
+
table: TanStackTable<TData>;
|
|
16
|
+
height?: number;
|
|
17
|
+
overscan?: number;
|
|
18
|
+
className?: string;
|
|
19
|
+
onVisibilityChange?: (visibleRange: { start: number; end: number }) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Memoized cell component for performance
|
|
24
|
+
*/
|
|
25
|
+
const MemoizedCell = memo(({ cell, style }: { cell: any; style?: React.CSSProperties }) => {
|
|
26
|
+
return (
|
|
27
|
+
<td
|
|
28
|
+
key={cell.id}
|
|
29
|
+
className={cn(
|
|
30
|
+
"px-4 py-2 text-sm border-b border-gray-200 overflow-hidden",
|
|
31
|
+
cell.column?.getCanSort && cell.column.getCanSort() && "cursor-pointer select-none"
|
|
32
|
+
)}
|
|
33
|
+
style={style}
|
|
34
|
+
>
|
|
35
|
+
{flexRender(cell.column?.columnDef?.cell, cell.getContext?.() || {})}
|
|
36
|
+
</td>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
MemoizedCell.displayName = 'MemoizedCell';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Memoized row component for performance
|
|
44
|
+
*/
|
|
45
|
+
const MemoizedRow = memo(({ row, style }: {
|
|
46
|
+
row: any;
|
|
47
|
+
style: React.CSSProperties;
|
|
48
|
+
}) => {
|
|
49
|
+
return (
|
|
50
|
+
<tr
|
|
51
|
+
key={row.id}
|
|
52
|
+
className={cn(
|
|
53
|
+
"hover:bg-app-sec-50 transition-colors",
|
|
54
|
+
row.getIsSelected && row.getIsSelected() && "bg-app-main-50"
|
|
55
|
+
)}
|
|
56
|
+
style={style}
|
|
57
|
+
data-testid={`data-table-row-${row.id}`}
|
|
58
|
+
>
|
|
59
|
+
{row.getVisibleCells && row.getVisibleCells().map((cell: any) => (
|
|
60
|
+
<MemoizedCell
|
|
61
|
+
key={cell.id}
|
|
62
|
+
cell={cell}
|
|
63
|
+
style={{}}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
</tr>
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
MemoizedRow.displayName = 'MemoizedRow';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* High-performance virtualized DataTable component with fixed column alignment
|
|
74
|
+
*/
|
|
75
|
+
export function VirtualizedDataTable<TData extends DataRecord>({
|
|
76
|
+
table,
|
|
77
|
+
height = 400,
|
|
78
|
+
overscan = 5,
|
|
79
|
+
className,
|
|
80
|
+
onVisibilityChange
|
|
81
|
+
}: VirtualizedDataTableProps<TData>) {
|
|
82
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
83
|
+
const headerRef = useRef<HTMLTableElement>(null);
|
|
84
|
+
const bodyRef = useRef<HTMLTableElement>(null);
|
|
85
|
+
const rows = table.getRowModel().rows;
|
|
86
|
+
|
|
87
|
+
// Virtual scrolling setup
|
|
88
|
+
const virtualizer = useVirtualizer({
|
|
89
|
+
count: rows.length,
|
|
90
|
+
getScrollElement: () => parentRef.current,
|
|
91
|
+
estimateSize: () => 40, // Estimated row height
|
|
92
|
+
overscan,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const virtualRows = virtualizer.getVirtualItems();
|
|
96
|
+
|
|
97
|
+
// Get table headers
|
|
98
|
+
const headerGroups = table.getHeaderGroups();
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
// Notify parent of visibility changes
|
|
102
|
+
const handleVisibilityChange = useCallback(() => {
|
|
103
|
+
if (virtualRows.length > 0 && onVisibilityChange) {
|
|
104
|
+
const start = virtualRows[0].index;
|
|
105
|
+
const end = virtualRows[virtualRows.length - 1].index;
|
|
106
|
+
onVisibilityChange({ start, end });
|
|
107
|
+
}
|
|
108
|
+
}, [virtualRows, onVisibilityChange]);
|
|
109
|
+
|
|
110
|
+
// Call visibility change handler when virtual rows change
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
handleVisibilityChange();
|
|
113
|
+
}, [handleVisibilityChange]);
|
|
114
|
+
|
|
115
|
+
// Calculate total size for proper scrollbar
|
|
116
|
+
const totalSize = virtualizer.getTotalSize();
|
|
117
|
+
|
|
118
|
+
// Handle empty state
|
|
119
|
+
if (rows.length === 0) {
|
|
120
|
+
return (
|
|
121
|
+
<div className={cn("border rounded-lg overflow-hidden", className)}>
|
|
122
|
+
{/* Fixed Header */}
|
|
123
|
+
<div className="bg-app-sec-50 border-b">
|
|
124
|
+
<table ref={headerRef} className="w-full table-fixed">
|
|
125
|
+
<thead>
|
|
126
|
+
{headerGroups.map((headerGroup) => (
|
|
127
|
+
<tr key={headerGroup.id}>
|
|
128
|
+
{headerGroup.headers.map((header) => (
|
|
129
|
+
<th
|
|
130
|
+
key={header.id}
|
|
131
|
+
className={cn(
|
|
132
|
+
"px-4 py-3 text-left text-xs font-medium text-app-sec-500 uppercase tracking-wider",
|
|
133
|
+
header.column?.getCanSort && header.column.getCanSort() && "cursor-pointer select-none hover:bg-app-sec-100"
|
|
134
|
+
)}
|
|
135
|
+
style={{}}
|
|
136
|
+
onClick={header.column?.getToggleSortingHandler ? header.column.getToggleSortingHandler() : undefined}
|
|
137
|
+
>
|
|
138
|
+
<div className="flex items-center space-x-1">
|
|
139
|
+
{header.isPlaceholder
|
|
140
|
+
? null
|
|
141
|
+
: flexRender(header.column?.columnDef?.header, header.getContext?.() || {})}
|
|
142
|
+
{header.column?.getCanSort && header.column.getCanSort() && (
|
|
143
|
+
<span className="ml-1">
|
|
144
|
+
{{
|
|
145
|
+
asc: '↑',
|
|
146
|
+
desc: '↓',
|
|
147
|
+
}[header.column?.getIsSorted ? header.column.getIsSorted() as string : ''] ?? '↕'}
|
|
148
|
+
</span>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
</th>
|
|
152
|
+
))}
|
|
153
|
+
</tr>
|
|
154
|
+
))}
|
|
155
|
+
</thead>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Empty State */}
|
|
160
|
+
<div className="flex items-center justify-center py-12">
|
|
161
|
+
<div className="text-center">
|
|
162
|
+
<div className="text-app-sec-400 text-lg mb-2">📊</div>
|
|
163
|
+
<p className="text-app-sec-500">No data available</p>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className={cn("border rounded-lg overflow-hidden", className)}>
|
|
172
|
+
{/* Fixed Header */}
|
|
173
|
+
<div className="bg-app-sec-50 border-b sticky top-0 z-10">
|
|
174
|
+
<table ref={headerRef} className="w-full table-fixed">
|
|
175
|
+
<thead>
|
|
176
|
+
{headerGroups.map((headerGroup) => (
|
|
177
|
+
<tr key={headerGroup.id}>
|
|
178
|
+
{headerGroup.headers.map((header) => (
|
|
179
|
+
<th
|
|
180
|
+
key={header.id}
|
|
181
|
+
className={cn(
|
|
182
|
+
"px-4 py-3 text-left text-xs font-medium text-app-sec-500 uppercase tracking-wider",
|
|
183
|
+
header.column?.getCanSort && header.column.getCanSort() && "cursor-pointer select-none hover:bg-app-sec-100"
|
|
184
|
+
)}
|
|
185
|
+
style={{}}
|
|
186
|
+
onClick={header.column?.getToggleSortingHandler ? header.column.getToggleSortingHandler() : undefined}
|
|
187
|
+
>
|
|
188
|
+
<div className="flex items-center space-x-1">
|
|
189
|
+
{header.isPlaceholder
|
|
190
|
+
? null
|
|
191
|
+
: flexRender(header.column?.columnDef?.header, header.getContext?.() || {})}
|
|
192
|
+
{header.column?.getCanSort && header.column.getCanSort() && (
|
|
193
|
+
<span className="ml-1">
|
|
194
|
+
{{
|
|
195
|
+
asc: '↑',
|
|
196
|
+
desc: '↓',
|
|
197
|
+
}[header.column?.getIsSorted ? header.column.getIsSorted() as string : ''] ?? '↕'}
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
</th>
|
|
202
|
+
))}
|
|
203
|
+
</tr>
|
|
204
|
+
))}
|
|
205
|
+
</thead>
|
|
206
|
+
</table>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{/* Virtualized Body */}
|
|
210
|
+
<div
|
|
211
|
+
ref={parentRef}
|
|
212
|
+
className="overflow-auto"
|
|
213
|
+
style={{ height: `${height}px` }}
|
|
214
|
+
>
|
|
215
|
+
<div
|
|
216
|
+
style={{
|
|
217
|
+
height: `${totalSize}px`,
|
|
218
|
+
width: '100%',
|
|
219
|
+
position: 'relative',
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<table ref={bodyRef} className="w-full table-fixed">
|
|
223
|
+
<tbody>
|
|
224
|
+
{virtualRows.map((virtualRow) => {
|
|
225
|
+
const row = rows[virtualRow.index];
|
|
226
|
+
if (!row) return null;
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<MemoizedRow
|
|
230
|
+
key={row.id}
|
|
231
|
+
row={row}
|
|
232
|
+
style={{
|
|
233
|
+
position: 'absolute',
|
|
234
|
+
top: 0,
|
|
235
|
+
left: 0,
|
|
236
|
+
width: '100%',
|
|
237
|
+
height: `${virtualRow.size}px`,
|
|
238
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
239
|
+
}}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
</tbody>
|
|
244
|
+
</table>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Footer with row count */}
|
|
249
|
+
<div className="bg-app-sec-50 border-t px-4 py-2">
|
|
250
|
+
<div className="flex items-center justify-between text-sm text-app-sec-500">
|
|
251
|
+
<span>
|
|
252
|
+
Showing {virtualRows.length > 0 ? virtualRows[0].index + 1 : 0} to{' '}
|
|
253
|
+
{virtualRows.length > 0 ? virtualRows[virtualRows.length - 1].index + 1 : 0} of{' '}
|
|
254
|
+
{rows.length} rows
|
|
255
|
+
</span>
|
|
256
|
+
<span>
|
|
257
|
+
Virtual rows: {virtualRows.length} / {rows.length}
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Performance-optimized virtualized table with additional features
|
|
267
|
+
*/
|
|
268
|
+
export interface EnhancedVirtualizedDataTableProps<TData extends DataRecord>
|
|
269
|
+
extends VirtualizedDataTableProps<TData> {
|
|
270
|
+
enableRowSelection?: boolean;
|
|
271
|
+
enableHover?: boolean;
|
|
272
|
+
stickyHeader?: boolean;
|
|
273
|
+
loadingRows?: number;
|
|
274
|
+
isLoading?: boolean;
|
|
275
|
+
emptyMessage?: string;
|
|
276
|
+
// Additional props for DataTable integration
|
|
277
|
+
variant?: 'default' | 'compact' | 'spacious';
|
|
278
|
+
enableGrouping?: boolean;
|
|
279
|
+
actions?: any[];
|
|
280
|
+
enableEditing?: boolean;
|
|
281
|
+
enableDeletion?: boolean;
|
|
282
|
+
onEditRow?: (row: TData, data: Partial<TData>) => void;
|
|
283
|
+
onDeleteRow?: (row: TData) => void;
|
|
284
|
+
getRowId?: (row: any) => string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function EnhancedVirtualizedDataTable<TData extends DataRecord>({
|
|
288
|
+
table,
|
|
289
|
+
height = 400,
|
|
290
|
+
overscan = 5,
|
|
291
|
+
className,
|
|
292
|
+
enableRowSelection = false,
|
|
293
|
+
enableHover = true,
|
|
294
|
+
stickyHeader = true,
|
|
295
|
+
loadingRows = 10,
|
|
296
|
+
isLoading = false,
|
|
297
|
+
emptyMessage = "No data available",
|
|
298
|
+
onVisibilityChange,
|
|
299
|
+
// Additional props for DataTable integration
|
|
300
|
+
variant = 'default',
|
|
301
|
+
enableGrouping = false,
|
|
302
|
+
actions = [],
|
|
303
|
+
enableEditing = false,
|
|
304
|
+
enableDeletion = false,
|
|
305
|
+
onEditRow,
|
|
306
|
+
onDeleteRow,
|
|
307
|
+
getRowId
|
|
308
|
+
}: EnhancedVirtualizedDataTableProps<TData>) {
|
|
309
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
310
|
+
const headerRef = useRef<HTMLTableElement>(null);
|
|
311
|
+
const bodyRef = useRef<HTMLTableElement>(null);
|
|
312
|
+
const rows = table.getRowModel().rows;
|
|
313
|
+
|
|
314
|
+
// Show loading skeleton if loading
|
|
315
|
+
const displayRows = useMemo(() => {
|
|
316
|
+
if (isLoading) {
|
|
317
|
+
return Array.from({ length: loadingRows }, (_, index) => ({
|
|
318
|
+
id: `loading-${index}`,
|
|
319
|
+
isLoading: true,
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
return rows;
|
|
323
|
+
}, [isLoading, loadingRows, rows]);
|
|
324
|
+
|
|
325
|
+
const virtualizer = useVirtualizer({
|
|
326
|
+
count: displayRows.length,
|
|
327
|
+
getScrollElement: () => parentRef.current,
|
|
328
|
+
estimateSize: () => 40,
|
|
329
|
+
overscan,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const virtualRows = virtualizer.getVirtualItems();
|
|
333
|
+
const headerGroups = table.getHeaderGroups();
|
|
334
|
+
const totalSize = virtualizer.getTotalSize();
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
// Handle visibility changes
|
|
338
|
+
React.useEffect(() => {
|
|
339
|
+
if (virtualRows.length > 0 && onVisibilityChange && !isLoading) {
|
|
340
|
+
const start = virtualRows[0].index;
|
|
341
|
+
const end = virtualRows[virtualRows.length - 1].index;
|
|
342
|
+
onVisibilityChange({ start, end });
|
|
343
|
+
}
|
|
344
|
+
}, [virtualRows, onVisibilityChange, isLoading]);
|
|
345
|
+
|
|
346
|
+
// Loading row component
|
|
347
|
+
const LoadingRow = memo(({ style }: { style: React.CSSProperties }) => (
|
|
348
|
+
<tr style={style} className="animate-pulse">
|
|
349
|
+
{headerGroups[0]?.headers.map((header) => (
|
|
350
|
+
<td
|
|
351
|
+
key={header.id}
|
|
352
|
+
className="px-4 py-2 border-b border-app-sec-200"
|
|
353
|
+
style={{}}
|
|
354
|
+
>
|
|
355
|
+
<div className="h-4 bg-app-sec-200 rounded"></div>
|
|
356
|
+
</td>
|
|
357
|
+
))}
|
|
358
|
+
</tr>
|
|
359
|
+
));
|
|
360
|
+
|
|
361
|
+
LoadingRow.displayName = 'LoadingRow';
|
|
362
|
+
|
|
363
|
+
// Empty state
|
|
364
|
+
if (!isLoading && rows.length === 0) {
|
|
365
|
+
return (
|
|
366
|
+
<div className={cn("border rounded-lg overflow-hidden", className)} data-testid="enhanced-virtualized-table">
|
|
367
|
+
<div className="bg-app-sec-50 border-b">
|
|
368
|
+
<table ref={headerRef} className="w-full table-fixed">
|
|
369
|
+
<thead>
|
|
370
|
+
{headerGroups.map((headerGroup) => (
|
|
371
|
+
<tr key={headerGroup.id}>
|
|
372
|
+
{headerGroup.headers.map((header) => (
|
|
373
|
+
<th
|
|
374
|
+
key={header.id}
|
|
375
|
+
className="px-4 py-3 text-left text-xs font-medium text-app-sec-500 uppercase tracking-wider"
|
|
376
|
+
style={{}}
|
|
377
|
+
>
|
|
378
|
+
{header.isPlaceholder
|
|
379
|
+
? null
|
|
380
|
+
: flexRender(header.column?.columnDef?.header, header.getContext?.() || {})}
|
|
381
|
+
</th>
|
|
382
|
+
))}
|
|
383
|
+
</tr>
|
|
384
|
+
))}
|
|
385
|
+
</thead>
|
|
386
|
+
</table>
|
|
387
|
+
</div>
|
|
388
|
+
<div className="flex items-center justify-center py-12">
|
|
389
|
+
<div className="text-center">
|
|
390
|
+
<div className="text-app-sec-400 text-lg mb-2">📊</div>
|
|
391
|
+
<p className="text-app-sec-500">{emptyMessage}</p>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<div className={cn("border rounded-lg overflow-hidden", className)} data-testid="enhanced-virtualized-table">
|
|
400
|
+
{/* Sticky Header */}
|
|
401
|
+
<div className={cn("bg-app-sec-50 border-b", stickyHeader && "sticky top-0 z-10")}>
|
|
402
|
+
<table ref={headerRef} className="w-full table-fixed">
|
|
403
|
+
<thead>
|
|
404
|
+
{headerGroups.map((headerGroup) => (
|
|
405
|
+
<tr key={headerGroup.id}>
|
|
406
|
+
{headerGroup.headers.map((header) => (
|
|
407
|
+
<th
|
|
408
|
+
key={header.id}
|
|
409
|
+
className={cn(
|
|
410
|
+
"px-4 py-3 text-left text-xs font-medium text-app-sec-500 uppercase tracking-wider",
|
|
411
|
+
header.column?.getCanSort && header.column.getCanSort() && "cursor-pointer select-none hover:bg-app-sec-100"
|
|
412
|
+
)}
|
|
413
|
+
style={{}}
|
|
414
|
+
onClick={header.column?.getToggleSortingHandler ? header.column.getToggleSortingHandler() : undefined}
|
|
415
|
+
>
|
|
416
|
+
<div className="flex items-center space-x-1">
|
|
417
|
+
{header.isPlaceholder
|
|
418
|
+
? null
|
|
419
|
+
: flexRender(header.column?.columnDef?.header, header.getContext?.() || {})}
|
|
420
|
+
{header.column?.getCanSort && header.column.getCanSort() && (
|
|
421
|
+
<span className="ml-1">
|
|
422
|
+
{{
|
|
423
|
+
asc: '↑',
|
|
424
|
+
desc: '↓',
|
|
425
|
+
}[header.column?.getIsSorted ? header.column.getIsSorted() as string : ''] ?? '↕'}
|
|
426
|
+
</span>
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
</th>
|
|
430
|
+
))}
|
|
431
|
+
</tr>
|
|
432
|
+
))}
|
|
433
|
+
</thead>
|
|
434
|
+
</table>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
{/* Virtualized Body */}
|
|
438
|
+
<div
|
|
439
|
+
ref={parentRef}
|
|
440
|
+
className="overflow-auto"
|
|
441
|
+
style={{ height: `${height}px` }}
|
|
442
|
+
>
|
|
443
|
+
<div
|
|
444
|
+
style={{
|
|
445
|
+
height: `${totalSize}px`,
|
|
446
|
+
width: '100%',
|
|
447
|
+
position: 'relative',
|
|
448
|
+
}}
|
|
449
|
+
>
|
|
450
|
+
<table ref={bodyRef} className="w-full table-fixed">
|
|
451
|
+
<tbody>
|
|
452
|
+
{virtualRows.map((virtualRow) => {
|
|
453
|
+
const displayRow = displayRows[virtualRow.index];
|
|
454
|
+
|
|
455
|
+
if ('isLoading' in displayRow) {
|
|
456
|
+
return (
|
|
457
|
+
<LoadingRow
|
|
458
|
+
key={displayRow.id}
|
|
459
|
+
style={{
|
|
460
|
+
position: 'absolute',
|
|
461
|
+
top: 0,
|
|
462
|
+
left: 0,
|
|
463
|
+
width: '100%',
|
|
464
|
+
height: `${virtualRow.size}px`,
|
|
465
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
466
|
+
}}
|
|
467
|
+
/>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const row = displayRow as any;
|
|
472
|
+
return (
|
|
473
|
+
<MemoizedRow
|
|
474
|
+
key={row.id}
|
|
475
|
+
row={row}
|
|
476
|
+
style={{
|
|
477
|
+
position: 'absolute',
|
|
478
|
+
top: 0,
|
|
479
|
+
left: 0,
|
|
480
|
+
width: '100%',
|
|
481
|
+
height: `${virtualRow.size}px`,
|
|
482
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
483
|
+
}}
|
|
484
|
+
/>
|
|
485
|
+
);
|
|
486
|
+
})}
|
|
487
|
+
</tbody>
|
|
488
|
+
</table>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
{/* Performance Footer */}
|
|
493
|
+
<div className="bg-app-sec-50 border-t px-4 py-2">
|
|
494
|
+
<div className="flex items-center justify-between text-sm text-app-sec-500">
|
|
495
|
+
<span>
|
|
496
|
+
{isLoading ? (
|
|
497
|
+
"Loading..."
|
|
498
|
+
) : (
|
|
499
|
+
<>
|
|
500
|
+
Showing {virtualRows.length > 0 ? virtualRows[0].index + 1 : 0} to{' '}
|
|
501
|
+
{virtualRows.length > 0 ? virtualRows[virtualRows.length - 1].index + 1 : 0} of{' '}
|
|
502
|
+
{rows.length} rows
|
|
503
|
+
</>
|
|
504
|
+
)}
|
|
505
|
+
</span>
|
|
506
|
+
<span>
|
|
507
|
+
Virtual: {virtualRows.length} / {displayRows.length}
|
|
508
|
+
</span>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
@@ -567,75 +567,21 @@ describe('[component] AccessDeniedPage', () => {
|
|
|
567
567
|
expect(handleRetry).toHaveBeenCalledTimes(3);
|
|
568
568
|
});
|
|
569
569
|
|
|
570
|
-
//
|
|
571
|
-
// This test
|
|
570
|
+
// Test SSR behavior where window might be undefined
|
|
571
|
+
// NOTE: This test is skipped because React DOM and user-event require window to exist.
|
|
572
|
+
// In jsdom, window is always defined, and stubbing it as undefined breaks React's internal
|
|
573
|
+
// event system. SSR behavior where window is undefined is tested in actual SSR environments
|
|
574
|
+
// (Next.js, Remix, etc.) where the component is server-rendered.
|
|
575
|
+
//
|
|
576
|
+
// If you need to test SSR behavior, use a proper SSR testing setup or test in actual SSR environments.
|
|
572
577
|
it.skip('handles undefined window object gracefully', () => {
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
clipboard: {
|
|
581
|
-
writeText: vi.fn(),
|
|
582
|
-
readText: vi.fn(),
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
// @ts-expect-error - Testing edge case
|
|
587
|
-
if (typeof global !== 'undefined') {
|
|
588
|
-
delete global.window;
|
|
589
|
-
delete global.navigator;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Ensure navigator still exists on globalThis for userEvent cleanup
|
|
593
|
-
if (typeof globalThis !== 'undefined') {
|
|
594
|
-
Object.defineProperty(globalThis, 'navigator', {
|
|
595
|
-
value: savedNavigator,
|
|
596
|
-
writable: true,
|
|
597
|
-
configurable: true,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
expect(() => {
|
|
602
|
-
render(<AccessDeniedPage resource="users" />);
|
|
603
|
-
}).not.toThrow();
|
|
604
|
-
|
|
605
|
-
// CRITICAL: Fully restore window and navigator to prevent breaking subsequent tests
|
|
606
|
-
if (typeof global !== 'undefined') {
|
|
607
|
-
if (savedWindow) {
|
|
608
|
-
global.window = savedWindow;
|
|
609
|
-
} else if (typeof window !== 'undefined') {
|
|
610
|
-
// Ensure window is restored even if it was undefined
|
|
611
|
-
Object.defineProperty(global, 'window', {
|
|
612
|
-
value: window,
|
|
613
|
-
writable: true,
|
|
614
|
-
configurable: true,
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (savedNavigator) {
|
|
619
|
-
global.navigator = savedNavigator;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Ensure navigator on globalThis and window
|
|
624
|
-
if (typeof globalThis !== 'undefined') {
|
|
625
|
-
Object.defineProperty(globalThis, 'navigator', {
|
|
626
|
-
value: savedNavigator,
|
|
627
|
-
writable: true,
|
|
628
|
-
configurable: true,
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (typeof window !== 'undefined') {
|
|
633
|
-
Object.defineProperty(window, 'navigator', {
|
|
634
|
-
value: savedNavigator,
|
|
635
|
-
writable: true,
|
|
636
|
-
configurable: true,
|
|
637
|
-
});
|
|
638
|
-
}
|
|
578
|
+
// This test cannot run in jsdom because:
|
|
579
|
+
// 1. React DOM requires window.event to exist
|
|
580
|
+
// 2. user-event cleanup hooks require window.navigator
|
|
581
|
+
// 3. jsdom always provides a window object
|
|
582
|
+
//
|
|
583
|
+
// SSR behavior is validated in actual SSR environments where window is truly undefined
|
|
584
|
+
// during server-side rendering.
|
|
639
585
|
});
|
|
640
586
|
});
|
|
641
587
|
|
|
@@ -83,6 +83,10 @@ const createMockColumn = (overrides: Partial<Column<any, unknown>> = {}): Column
|
|
|
83
83
|
getFilterValue: vi.fn(() => undefined),
|
|
84
84
|
setFilterValue: vi.fn(),
|
|
85
85
|
getCanFilter: vi.fn(() => true),
|
|
86
|
+
columnDef: {
|
|
87
|
+
header: undefined,
|
|
88
|
+
accessorKey: undefined,
|
|
89
|
+
},
|
|
86
90
|
...overrides,
|
|
87
91
|
} as unknown as Column<any, unknown>);
|
|
88
92
|
|
|
@@ -133,6 +137,64 @@ describe('[component] ColumnFilter', () => {
|
|
|
133
137
|
const input = screen.getByPlaceholderText('Search...');
|
|
134
138
|
expect(input).toBeInTheDocument();
|
|
135
139
|
});
|
|
140
|
+
|
|
141
|
+
it('uses column header text for placeholder when header is a string', () => {
|
|
142
|
+
const column = createMockColumn({
|
|
143
|
+
columnDef: {
|
|
144
|
+
header: 'Bulk Distribution',
|
|
145
|
+
accessorKey: 'item_bulkdistribution',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
render(<ColumnFilter column={column} />);
|
|
150
|
+
|
|
151
|
+
const input = screen.getByPlaceholderText('Filter Bulk Distribution...');
|
|
152
|
+
expect(input).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('falls back to accessorKey when header is not provided', () => {
|
|
156
|
+
const column = createMockColumn({
|
|
157
|
+
columnDef: {
|
|
158
|
+
header: undefined,
|
|
159
|
+
accessorKey: 'logistics_name',
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
render(<ColumnFilter column={column} />);
|
|
164
|
+
|
|
165
|
+
const input = screen.getByPlaceholderText('Filter logistics_name...');
|
|
166
|
+
expect(input).toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('falls back to column id when header and accessorKey are not provided', () => {
|
|
170
|
+
const column = createMockColumn({
|
|
171
|
+
id: 'fallback-column',
|
|
172
|
+
columnDef: {
|
|
173
|
+
header: undefined,
|
|
174
|
+
accessorKey: undefined,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
render(<ColumnFilter column={column} />);
|
|
179
|
+
|
|
180
|
+
const input = screen.getByPlaceholderText('Filter fallback-column...');
|
|
181
|
+
expect(input).toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('falls back to accessorKey when header is a function', () => {
|
|
185
|
+
const column = createMockColumn({
|
|
186
|
+
columnDef: {
|
|
187
|
+
header: () => 'Function Header',
|
|
188
|
+
accessorKey: 'unit_name',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
render(<ColumnFilter column={column} />);
|
|
193
|
+
|
|
194
|
+
// When header is a function, should fall back to accessorKey
|
|
195
|
+
const input = screen.getByPlaceholderText('Filter unit_name...');
|
|
196
|
+
expect(input).toBeInTheDocument();
|
|
197
|
+
});
|
|
136
198
|
});
|
|
137
199
|
|
|
138
200
|
describe('Text Filter', () => {
|