@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.
Files changed (292) hide show
  1. package/dist/{DataTable-CYOHOX3O.js → DataTable-JXFCA2BJ.js} +10 -9
  2. package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
  3. package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
  4. package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
  5. package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
  6. package/dist/chunk-4C7EXCAR.js.map +1 -0
  7. package/dist/{chunk-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
  8. package/dist/chunk-5JMOHWDI.js.map +1 -0
  9. package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
  10. package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
  11. package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
  12. package/dist/chunk-7QCC6MCP.js.map +1 -0
  13. package/dist/chunk-BJPBT3CU.js +21 -0
  14. package/dist/chunk-BJPBT3CU.js.map +1 -0
  15. package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
  16. package/dist/chunk-BOOI7GK2.js.map +1 -0
  17. package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
  18. package/dist/chunk-JISYG63F.js +70 -0
  19. package/dist/chunk-JISYG63F.js.map +1 -0
  20. package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
  21. package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
  22. package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
  23. package/dist/{chunk-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
  24. package/dist/chunk-TLT2ZR3L.js.map +1 -0
  25. package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
  26. package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
  27. package/dist/components.d.ts +1 -1
  28. package/dist/components.js +12 -11
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.js +8 -9
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +15 -14
  34. package/dist/index.js.map +1 -1
  35. package/dist/providers.js +3 -4
  36. package/dist/rbac/index.js +8 -9
  37. package/dist/schema-DTDZQe2u.d.ts +28 -0
  38. package/dist/types.d.ts +152 -3
  39. package/dist/types.js +51 -16
  40. package/dist/types.js.map +1 -1
  41. package/dist/utils.d.ts +89 -4
  42. package/dist/utils.js +214 -96
  43. package/dist/utils.js.map +1 -1
  44. package/dist/validation.d.ts +1 -343
  45. package/dist/validation.js +3 -100
  46. package/docs/api/classes/ColumnFactory.md +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +1 -1
  49. package/docs/api/classes/MissingUserContextError.md +1 -1
  50. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  51. package/docs/api/classes/PermissionDeniedError.md +1 -1
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +1 -1
  54. package/docs/api/classes/RBACCache.md +1 -1
  55. package/docs/api/classes/RBACEngine.md +1 -1
  56. package/docs/api/classes/RBACError.md +1 -1
  57. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  58. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  59. package/docs/api/classes/StorageUtils.md +1 -1
  60. package/docs/api/enums/FileCategory.md +1 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/BadgeProps.md +27 -0
  63. package/docs/api/interfaces/ButtonProps.md +1 -1
  64. package/docs/api/interfaces/CardProps.md +1 -1
  65. package/docs/api/interfaces/ColorPalette.md +1 -1
  66. package/docs/api/interfaces/ColorShade.md +1 -1
  67. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  68. package/docs/api/interfaces/DataRecord.md +1 -1
  69. package/docs/api/interfaces/DataTableAction.md +1 -1
  70. package/docs/api/interfaces/DataTableColumn.md +1 -1
  71. package/docs/api/interfaces/DataTableProps.md +1 -1
  72. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  73. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  74. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  75. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  76. package/docs/api/interfaces/EventLogoProps.md +1 -1
  77. package/docs/api/interfaces/ExportColumn.md +1 -1
  78. package/docs/api/interfaces/ExportOptions.md +1 -1
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  128. package/docs/api/interfaces/StorageConfig.md +1 -1
  129. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  130. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  131. package/docs/api/interfaces/StorageListOptions.md +1 -1
  132. package/docs/api/interfaces/StorageListResult.md +1 -1
  133. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  134. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  135. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  136. package/docs/api/interfaces/StyleImport.md +1 -1
  137. package/docs/api/interfaces/SwitchProps.md +1 -1
  138. package/docs/api/interfaces/ToastActionElement.md +1 -1
  139. package/docs/api/interfaces/ToastProps.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  141. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  143. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  145. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  147. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  148. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  150. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  151. package/docs/api/interfaces/UserEventAccess.md +1 -1
  152. package/docs/api/interfaces/UserMenuProps.md +1 -1
  153. package/docs/api/interfaces/UserProfile.md +1 -1
  154. package/docs/api/modules.md +84 -15
  155. package/docs/architecture/README.md +0 -1
  156. package/docs/styles/README.md +0 -2
  157. package/examples/RBAC/CompleteRBACExample.tsx +324 -0
  158. package/examples/RBAC/EventBasedApp.tsx +239 -0
  159. package/examples/RBAC/PermissionExample.tsx +151 -0
  160. package/examples/RBAC/index.ts +13 -0
  161. package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
  162. package/examples/public-pages/PublicEventPage.tsx +274 -0
  163. package/examples/public-pages/PublicPageApp.tsx +308 -0
  164. package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
  165. package/examples/public-pages/index.ts +14 -0
  166. package/package.json +1 -10
  167. package/src/__tests__/TEST_STANDARD.md +92 -0
  168. package/src/components/Badge/Badge.test.tsx +314 -0
  169. package/src/components/Badge/Badge.tsx +304 -0
  170. package/src/components/Badge/index.ts +3 -0
  171. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
  172. package/src/components/DataTable/__tests__/styles.test.ts +1 -1
  173. package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
  174. package/src/components/DataTable/components/DataTableBody.tsx +461 -0
  175. package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
  176. package/src/components/DataTable/components/FilterRow.tsx +9 -3
  177. package/src/components/DataTable/components/PaginationControls.tsx +1 -0
  178. package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
  179. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
  180. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
  181. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
  182. package/src/components/DataTable/core/ActionManager.ts +235 -0
  183. package/src/components/DataTable/core/ColumnManager.ts +205 -0
  184. package/src/components/DataTable/core/DataManager.ts +188 -0
  185. package/src/components/DataTable/core/DataTableContext.tsx +181 -0
  186. package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
  187. package/src/components/DataTable/core/PluginRegistry.ts +229 -0
  188. package/src/components/DataTable/core/StateManager.ts +311 -0
  189. package/src/components/DataTable/core/interfaces.ts +338 -0
  190. package/src/components/DataTable/styles.ts +27 -6
  191. package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
  192. package/src/components/DataTable/utils/columnUtils.ts +40 -0
  193. package/src/components/DataTable/utils/debugTools.ts +609 -0
  194. package/src/components/DataTable/utils/index.ts +1 -0
  195. package/src/components/Dialog/README.md +804 -0
  196. package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
  197. package/src/components/Dialog/utils/safeHtml.ts +185 -0
  198. package/src/components/Footer/Footer.test.tsx +1 -1
  199. package/src/components/Form/Form.test.tsx +1 -1
  200. package/src/components/Form/FormErrorSummary.tsx +113 -0
  201. package/src/components/Form/FormFieldset.tsx +127 -0
  202. package/src/components/Form/FormLiveRegion.tsx +198 -0
  203. package/src/components/LoginForm/LoginForm.test.tsx +1 -1
  204. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
  205. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  206. package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
  207. package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
  208. package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
  209. package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
  210. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  211. package/src/components/Select/Select.test.tsx +1 -1
  212. package/src/components/Select/Select.tsx +20 -8
  213. package/src/components/Table/__tests__/Table.test.tsx +1 -1
  214. package/src/components/index.ts +3 -0
  215. package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
  216. package/src/index.ts +4 -0
  217. package/src/rbac/hooks/useCan.test.ts +24 -0
  218. package/src/rbac/hooks/usePermissions.ts +49 -12
  219. package/src/styles/core.css +3 -0
  220. package/src/utils/appConfig.ts +47 -0
  221. package/src/utils/appIdResolver.test.ts +499 -0
  222. package/src/utils/appIdResolver.ts +130 -0
  223. package/src/utils/appNameResolver.simple.test.ts +212 -0
  224. package/src/utils/appNameResolver.test.ts +121 -0
  225. package/src/utils/appNameResolver.ts +191 -0
  226. package/src/utils/audit.ts +127 -0
  227. package/src/utils/auth-utils.ts +96 -0
  228. package/src/utils/bundleAnalysis.ts +129 -0
  229. package/src/utils/cn.ts +7 -0
  230. package/src/utils/debugLogger.ts +67 -0
  231. package/src/utils/deviceFingerprint.ts +215 -0
  232. package/src/utils/dynamicUtils.ts +105 -0
  233. package/src/utils/file-reference.test.ts +788 -0
  234. package/src/utils/file-reference.ts +519 -0
  235. package/src/utils/formatDate.test.ts +237 -0
  236. package/src/utils/formatting.ts +133 -0
  237. package/src/utils/index.ts +7 -0
  238. package/src/utils/lazyLoad.tsx +44 -0
  239. package/src/utils/logger.ts +179 -0
  240. package/src/utils/organisationContext.test.ts +322 -0
  241. package/src/utils/organisationContext.ts +153 -0
  242. package/src/utils/performanceBenchmark.ts +64 -0
  243. package/src/utils/performanceBudgets.ts +110 -0
  244. package/src/utils/permissionTypes.ts +37 -0
  245. package/src/utils/permissionUtils.test.ts +393 -0
  246. package/src/utils/permissionUtils.ts +34 -0
  247. package/src/utils/sanitization.ts +264 -0
  248. package/src/utils/schemaUtils.ts +37 -0
  249. package/src/utils/secureDataAccess.test.ts +711 -0
  250. package/src/utils/secureDataAccess.ts +377 -0
  251. package/src/utils/secureErrors.ts +79 -0
  252. package/src/utils/secureStorage.ts +244 -0
  253. package/src/utils/security.ts +156 -0
  254. package/src/utils/securityMonitor.ts +45 -0
  255. package/src/utils/sessionTracking.ts +126 -0
  256. package/src/utils/validation.ts +111 -0
  257. package/src/utils/validationUtils.ts +120 -0
  258. package/src/validation/index.ts +2 -2
  259. package/dist/chunk-444EZN6N.js.map +0 -1
  260. package/dist/chunk-APIBCTL2.js +0 -670
  261. package/dist/chunk-APIBCTL2.js.map +0 -1
  262. package/dist/chunk-HJGGOMQ6.js.map +0 -1
  263. package/dist/chunk-K2WWTH7O.js +0 -94
  264. package/dist/chunk-K2WWTH7O.js.map +0 -1
  265. package/dist/chunk-L6PGMCMD.js.map +0 -1
  266. package/dist/chunk-LMC26NLJ.js +0 -84
  267. package/dist/chunk-LMC26NLJ.js.map +0 -1
  268. package/dist/chunk-NOHEVYVX.js.map +0 -1
  269. package/dist/chunk-TVYPTYOY.js.map +0 -1
  270. package/dist/validation-8npbysjg.d.ts +0 -177
  271. /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-JXFCA2BJ.js.map} +0 -0
  272. /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
  273. /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
  274. /package/dist/{chunk-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
  275. /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
  276. /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
  277. /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
  278. /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
  279. /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
  280. /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
  281. /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
  282. /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
  283. /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
  284. /package/examples/{components → components 2}/DataTable/index.ts +0 -0
  285. /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
  286. /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
  287. /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
  288. /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
  289. /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
  290. /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
  291. /package/examples/{components → components 2}/Dialog/index.ts +0 -0
  292. /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
- // Move this test to the very end - it modifies global state and can break subsequent tests
571
- // This test MUST run last to avoid breaking other tests
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
- // Save original values
574
- const originalWindow = typeof window !== 'undefined' ? window : undefined;
575
- const originalNavigator = typeof navigator !== 'undefined' ? navigator : undefined;
576
-
577
- // Store references before deletion
578
- const savedWindow = originalWindow;
579
- const savedNavigator = originalNavigator || {
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', () => {