@jmruthers/pace-core 0.5.79 → 0.5.81

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 (146) hide show
  1. package/dist/{DataTable-BCBW5SCL.js → DataTable-OBT663FS.js} +6 -6
  2. package/dist/{UnifiedAuthProvider-TSHK77PL.js → UnifiedAuthProvider-K2IZAY5F.js} +3 -3
  3. package/dist/{chunk-TI67X46U.js → chunk-3FV24IOD.js} +7 -7
  4. package/dist/{chunk-WYKXRCXB.js → chunk-5BN3YGNK.js} +157 -53
  5. package/dist/{chunk-WYKXRCXB.js.map → chunk-5BN3YGNK.js.map} +1 -1
  6. package/dist/{chunk-RMK6FOHF.js → chunk-CACHCRZS.js} +158 -79
  7. package/dist/{chunk-RMK6FOHF.js.map → chunk-CACHCRZS.js.map} +1 -1
  8. package/dist/{chunk-GXWREXH7.js → chunk-CBSD3BZ3.js} +2 -2
  9. package/dist/{chunk-CALYF6HH.js → chunk-I2VVV5PQ.js} +2 -2
  10. package/dist/{chunk-3WFKFBVQ.js → chunk-KUYWZVR2.js} +4 -4
  11. package/dist/{chunk-LVV6J6ZF.js → chunk-NTW3KGS4.js} +5 -5
  12. package/dist/{chunk-HQ7KTKC3.js → chunk-RIXPZJUB.js} +2 -2
  13. package/dist/{chunk-JYCP4L55.js → chunk-S3JKDMD5.js} +3 -3
  14. package/dist/{chunk-OBXLAL3J.js → chunk-V5SWX6KL.js} +4 -4
  15. package/dist/{chunk-IQFITAE3.js → chunk-YVUZWLQG.js} +3 -3
  16. package/dist/components.js +8 -8
  17. package/dist/hooks.js +7 -7
  18. package/dist/index.js +11 -11
  19. package/dist/providers.js +2 -2
  20. package/dist/rbac/index.js +7 -7
  21. package/dist/utils.js +1 -1
  22. package/docs/api/classes/ColumnFactory.md +1 -1
  23. package/docs/api/classes/ErrorBoundary.md +1 -1
  24. package/docs/api/classes/InvalidScopeError.md +1 -1
  25. package/docs/api/classes/MissingUserContextError.md +1 -1
  26. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  27. package/docs/api/classes/PermissionDeniedError.md +1 -1
  28. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  29. package/docs/api/classes/RBACAuditManager.md +1 -1
  30. package/docs/api/classes/RBACCache.md +1 -1
  31. package/docs/api/classes/RBACEngine.md +1 -1
  32. package/docs/api/classes/RBACError.md +1 -1
  33. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  34. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  35. package/docs/api/classes/StorageUtils.md +1 -1
  36. package/docs/api/enums/FileCategory.md +1 -1
  37. package/docs/api/interfaces/AggregateConfig.md +1 -1
  38. package/docs/api/interfaces/ButtonProps.md +1 -1
  39. package/docs/api/interfaces/CardProps.md +1 -1
  40. package/docs/api/interfaces/ColorPalette.md +1 -1
  41. package/docs/api/interfaces/ColorShade.md +1 -1
  42. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  43. package/docs/api/interfaces/DataRecord.md +1 -1
  44. package/docs/api/interfaces/DataTableAction.md +1 -1
  45. package/docs/api/interfaces/DataTableColumn.md +1 -1
  46. package/docs/api/interfaces/DataTableProps.md +1 -1
  47. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  48. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  49. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  50. package/docs/api/interfaces/EventLogoProps.md +1 -1
  51. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  52. package/docs/api/interfaces/FileMetadata.md +1 -1
  53. package/docs/api/interfaces/FileReference.md +1 -1
  54. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  55. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  56. package/docs/api/interfaces/FileUploadProps.md +1 -1
  57. package/docs/api/interfaces/FooterProps.md +1 -1
  58. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  59. package/docs/api/interfaces/InputProps.md +1 -1
  60. package/docs/api/interfaces/LabelProps.md +1 -1
  61. package/docs/api/interfaces/LoginFormProps.md +1 -1
  62. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  63. package/docs/api/interfaces/NavigationContextType.md +1 -1
  64. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  65. package/docs/api/interfaces/NavigationItem.md +1 -1
  66. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  67. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  68. package/docs/api/interfaces/Organisation.md +1 -1
  69. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  70. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  71. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  72. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  73. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  74. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  75. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  76. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  77. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  78. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  79. package/docs/api/interfaces/PaletteData.md +1 -1
  80. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  81. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  82. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  83. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  84. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  85. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  86. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  87. package/docs/api/interfaces/RBACConfig.md +1 -1
  88. package/docs/api/interfaces/RBACLogger.md +1 -1
  89. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  90. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  91. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  92. package/docs/api/interfaces/RouteConfig.md +1 -1
  93. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  94. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  95. package/docs/api/interfaces/StorageConfig.md +1 -1
  96. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  97. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  98. package/docs/api/interfaces/StorageListOptions.md +1 -1
  99. package/docs/api/interfaces/StorageListResult.md +1 -1
  100. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  101. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  102. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  103. package/docs/api/interfaces/StyleImport.md +1 -1
  104. package/docs/api/interfaces/SwitchProps.md +1 -1
  105. package/docs/api/interfaces/ToastActionElement.md +1 -1
  106. package/docs/api/interfaces/ToastProps.md +1 -1
  107. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  108. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  109. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  110. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  111. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  112. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  113. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  114. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  115. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  116. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  117. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  118. package/docs/api/interfaces/UserEventAccess.md +1 -1
  119. package/docs/api/interfaces/UserMenuProps.md +1 -1
  120. package/docs/api/interfaces/UserProfile.md +1 -1
  121. package/docs/api/modules.md +3 -3
  122. package/docs/architecture/rpc-function-standards.md +757 -0
  123. package/package.json +1 -1
  124. package/src/components/DataTable/__tests__/styles.test.ts +5 -5
  125. package/src/components/DataTable/components/DataTableCore.tsx +125 -18
  126. package/src/components/DataTable/components/PaginationControls.tsx +78 -77
  127. package/src/components/DataTable/components/UnifiedTableBody.tsx +12 -4
  128. package/src/components/DataTable/hooks/useDataTableConfiguration.ts +29 -16
  129. package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +6 -4
  130. package/src/components/DataTable/hooks/useTableColumns.ts +20 -19
  131. package/src/components/DataTable/hooks/useTableHandlers.ts +8 -3
  132. package/src/components/DataTable/styles.ts +1 -1
  133. package/src/providers/services/UnifiedAuthProvider.tsx +175 -54
  134. package/src/styles/core.css +1 -2
  135. package/src/styles/base.css +0 -208
  136. /package/dist/{DataTable-BCBW5SCL.js.map → DataTable-OBT663FS.js.map} +0 -0
  137. /package/dist/{UnifiedAuthProvider-TSHK77PL.js.map → UnifiedAuthProvider-K2IZAY5F.js.map} +0 -0
  138. /package/dist/{chunk-TI67X46U.js.map → chunk-3FV24IOD.js.map} +0 -0
  139. /package/dist/{chunk-GXWREXH7.js.map → chunk-CBSD3BZ3.js.map} +0 -0
  140. /package/dist/{chunk-CALYF6HH.js.map → chunk-I2VVV5PQ.js.map} +0 -0
  141. /package/dist/{chunk-3WFKFBVQ.js.map → chunk-KUYWZVR2.js.map} +0 -0
  142. /package/dist/{chunk-LVV6J6ZF.js.map → chunk-NTW3KGS4.js.map} +0 -0
  143. /package/dist/{chunk-HQ7KTKC3.js.map → chunk-RIXPZJUB.js.map} +0 -0
  144. /package/dist/{chunk-JYCP4L55.js.map → chunk-S3JKDMD5.js.map} +0 -0
  145. /package/dist/{chunk-OBXLAL3J.js.map → chunk-V5SWX6KL.js.map} +0 -0
  146. /package/dist/{chunk-IQFITAE3.js.map → chunk-YVUZWLQG.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.79",
3
+ "version": "0.5.81",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -51,7 +51,7 @@ describe('DataTable Styles', () => {
51
51
  });
52
52
 
53
53
  it('has correct table base styles', () => {
54
- expect(tableStyles.table).toBe('w-full caption-bottom text-sm');
54
+ expect(tableStyles.table).toBe('w-full caption-top text-sm');
55
55
  expect(tableStyles.tableFixed).toBe('w-full table-fixed');
56
56
  });
57
57
 
@@ -219,7 +219,7 @@ describe('DataTable Styles', () => {
219
219
  describe('getTableClasses', () => {
220
220
  it('returns default table classes', () => {
221
221
  const result = getTableClasses();
222
- expect(result).toBe('w-full caption-bottom text-sm');
222
+ expect(result).toBe('w-full caption-top text-sm');
223
223
  });
224
224
 
225
225
  it('returns fixed table classes when isFixed is true', () => {
@@ -229,17 +229,17 @@ describe('DataTable Styles', () => {
229
229
 
230
230
  it('returns compact variant classes', () => {
231
231
  const result = getTableClasses({ variant: 'compact' });
232
- expect(result).toBe('w-full caption-bottom text-sm text-sm');
232
+ expect(result).toBe('w-full caption-top text-sm text-sm');
233
233
  });
234
234
 
235
235
  it('returns spacious variant classes', () => {
236
236
  const result = getTableClasses({ variant: 'spacious' });
237
- expect(result).toBe('w-full caption-bottom text-sm text-base');
237
+ expect(result).toBe('w-full caption-top text-sm text-base');
238
238
  });
239
239
 
240
240
  it('adds custom className', () => {
241
241
  const result = getTableClasses({ className: 'custom-table-class' });
242
- expect(result).toBe('w-full caption-bottom text-sm custom-table-class');
242
+ expect(result).toBe('w-full caption-top text-sm custom-table-class');
243
243
  });
244
244
 
245
245
  it('combines all options correctly', () => {
@@ -412,9 +412,32 @@ function DataTableInternal<TData extends DataRecord>({
412
412
  savedColumnOrder &&
413
413
  savedColumnOrder.length > 0
414
414
  ) {
415
- stateActions.setColumnOrder(savedColumnOrder);
415
+ // Normalize: ensure 'select' is first if selection is enabled
416
+ const normalizedOrder = secureFeatures.selection && savedColumnOrder.includes('select')
417
+ ? ['select', ...savedColumnOrder.filter(id => id !== 'select')]
418
+ : savedColumnOrder;
419
+ stateActions.setColumnOrder(normalizedOrder);
416
420
  }
417
- }, [secureFeatures.columnReordering, isColumnOrderLoaded, savedColumnOrder, stateActions]);
421
+ }, [secureFeatures.columnReordering, secureFeatures.selection, isColumnOrderLoaded, savedColumnOrder, stateActions]);
422
+
423
+ // CRITICAL: Always ensure state.columnOrder has 'select' first if selection is enabled
424
+ // This fixes any state that gets out of sync (e.g., from localStorage or user reordering)
425
+ useEffect(() => {
426
+ if (secureFeatures.selection && state.columnOrder.includes('select') && state.columnOrder[0] !== 'select') {
427
+ const normalizedOrder = ['select', ...state.columnOrder.filter(id => id !== 'select')];
428
+ if (import.meta.env?.MODE === 'development') {
429
+ console.warn('[DataTable] Correcting column order state - moving select to first position:', {
430
+ before: state.columnOrder,
431
+ after: normalizedOrder
432
+ });
433
+ }
434
+ stateActions.setColumnOrder(normalizedOrder);
435
+ // Also update persisted order if persistence is enabled
436
+ if (secureFeatures.columnReordering) {
437
+ updateColumnOrder(normalizedOrder);
438
+ }
439
+ }
440
+ }, [secureFeatures.selection, secureFeatures.columnReordering, state.columnOrder, stateActions, updateColumnOrder]);
418
441
 
419
442
  // ============================================================================
420
443
  // CONFIGURATION RESOLUTION - ALWAYS call these hooks
@@ -445,6 +468,56 @@ function DataTableInternal<TData extends DataRecord>({
445
468
  return closestOption;
446
469
  }, [initialPageSize, finalPageSizeOptions, secureFeatures.pagination]);
447
470
 
471
+ // Determine the effective pageSize to use (validated or current state)
472
+ // CRITICAL: This ensures we always pass a valid pageSize to TanStack Table configuration.
473
+ // An invalid pageSize can cause getPaginationRowModel() to return empty rows.
474
+ const effectivePageSize = useMemo(() => {
475
+ if (!secureFeatures.pagination || !finalPageSizeOptions.length) {
476
+ return state.pagination.pageSize;
477
+ }
478
+
479
+ // If current pageSize is invalid (not in options), use validated value immediately
480
+ // This is a safety net in case the useEffect hasn't run yet
481
+ if (!finalPageSizeOptions.includes(state.pagination.pageSize)) {
482
+ return validatedInitialPageSize;
483
+ }
484
+
485
+ return state.pagination.pageSize;
486
+ }, [state.pagination.pageSize, validatedInitialPageSize, secureFeatures.pagination, finalPageSizeOptions]);
487
+
488
+ // CRITICAL FIX: Ensure pagination state always uses a valid pageSize and pageIndex
489
+ // An invalid pageSize (not in page size options) causes getPaginationRowModel() to return empty rows,
490
+ // which manifests as "DataTable shows record count but no rows" bug for large datasets.
491
+ // An out-of-bounds pageIndex (beyond available pages) also causes empty rows.
492
+ // This fixes the bug where DataTable shows record count but no rows for large datasets.
493
+ useEffect(() => {
494
+ if (secureFeatures.pagination && finalPageSizeOptions.length > 0) {
495
+ const needsFix = !finalPageSizeOptions.includes(state.pagination.pageSize);
496
+
497
+ // Also check if pageIndex is out of bounds for the current data
498
+ const currentPageSize = effectivePageSize || validatedInitialPageSize;
499
+ const totalPages = currentPageSize > 0 ? Math.ceil(finalDataCount / currentPageSize) : 0;
500
+ const pageIndexOutOfBounds = totalPages > 0 && state.pagination.pageIndex >= totalPages;
501
+
502
+ if (needsFix || pageIndexOutOfBounds) {
503
+ // PageSize is invalid OR pageIndex is out of bounds - correct both to prevent empty rows
504
+ stateActions.setPagination({
505
+ pageSize: validatedInitialPageSize,
506
+ pageIndex: 0, // Reset to first page when correcting pagination issues
507
+ });
508
+ }
509
+ }
510
+ }, [
511
+ secureFeatures.pagination,
512
+ finalPageSizeOptions,
513
+ state.pagination.pageSize,
514
+ state.pagination.pageIndex,
515
+ validatedInitialPageSize,
516
+ stateActions,
517
+ effectivePageSize,
518
+ finalDataCount
519
+ ]);
520
+
448
521
  const isLoading = externalIsLoading || performanceLoading;
449
522
 
450
523
  // ============================================================================
@@ -607,34 +680,68 @@ function DataTableInternal<TData extends DataRecord>({
607
680
  return result;
608
681
  }, [actions, secureFeatures, permissions, secureHandlers, resolvedGetRowId, stateActions, data]);
609
682
 
683
+ // Normalize columnOrder for useTableColumns: ensure 'select' is always first
684
+ const normalizedColumnOrderForColumns = useMemo(() => {
685
+ if (secureFeatures.selection && state.columnOrder.includes('select')) {
686
+ return ['select', ...state.columnOrder.filter(id => id !== 'select')];
687
+ }
688
+ return state.columnOrder;
689
+ }, [state.columnOrder, secureFeatures.selection]);
690
+
610
691
  // MANDATORY: Process columns with actions
611
692
  const { enhancedColumns } = useTableColumns({
612
693
  columns,
613
694
  features: secureFeatures,
614
695
  effectiveActions,
615
- columnOrder: state.columnOrder
696
+ columnOrder: normalizedColumnOrderForColumns
616
697
  });
617
698
 
618
- const tableStateSnapshot = useMemo<TableStateSnapshot<TData>>(() => ({
619
- sorting: state.sorting,
620
- columnFilters: state.columnFilters,
621
- columnVisibility: state.columnVisibility,
622
- rowSelection,
623
- grouping: state.grouping,
624
- expanded: state.expanded,
625
- pagination: state.pagination,
626
- globalFilter: searchQuery,
627
- columnOrder: state.columnOrder,
628
- }), [
699
+ // Use effective pageSize in pagination state snapshot to ensure table receives valid pageSize
700
+ const paginationStateWithValidatedSize = useMemo(() => ({
701
+ ...state.pagination,
702
+ pageSize: effectivePageSize,
703
+ }), [state.pagination, effectivePageSize]);
704
+
705
+ const tableStateSnapshot = useMemo<TableStateSnapshot<TData>>(() => {
706
+ // Normalize columnOrder in snapshot: ensure 'select' is always first if selection is enabled
707
+ const normalizedColumnOrder = secureFeatures.selection && state.columnOrder.includes('select')
708
+ ? ['select', ...state.columnOrder.filter(id => id !== 'select')]
709
+ : state.columnOrder;
710
+
711
+ // Debug logging in dev mode
712
+ if (import.meta.env?.MODE === 'development' && secureFeatures.selection) {
713
+ if (state.columnOrder[0] !== 'select') {
714
+ console.warn('[DataTable] Column order normalized:', {
715
+ original: state.columnOrder,
716
+ normalized: normalizedColumnOrder,
717
+ firstColumnOriginal: state.columnOrder[0],
718
+ firstColumnNormalized: normalizedColumnOrder[0]
719
+ });
720
+ }
721
+ }
722
+
723
+ return {
724
+ sorting: state.sorting,
725
+ columnFilters: state.columnFilters,
726
+ columnVisibility: state.columnVisibility,
727
+ rowSelection,
728
+ grouping: state.grouping,
729
+ expanded: state.expanded,
730
+ pagination: paginationStateWithValidatedSize,
731
+ globalFilter: searchQuery,
732
+ columnOrder: normalizedColumnOrder,
733
+ };
734
+ }, [
629
735
  state.sorting,
630
736
  state.columnFilters,
631
737
  state.columnVisibility,
632
738
  rowSelection,
633
739
  state.grouping,
634
740
  state.expanded,
635
- state.pagination,
741
+ paginationStateWithValidatedSize,
636
742
  searchQuery,
637
743
  state.columnOrder,
744
+ secureFeatures.selection,
638
745
  ]);
639
746
 
640
747
  const tableHandlers = useTableHandlers({
@@ -660,7 +767,7 @@ function DataTableInternal<TData extends DataRecord>({
660
767
  getRowId: resolvedGetRowId,
661
768
  finalPaginationMode,
662
769
  finalDataCount,
663
- pageSize: state.pagination.pageSize,
770
+ pageSize: effectivePageSize,
664
771
  });
665
772
 
666
773
  const table = useReactTable(tableConfig);
@@ -705,7 +812,7 @@ function DataTableInternal<TData extends DataRecord>({
705
812
  {/* Table with semantic HTML structure */}
706
813
  <table
707
814
  className={getTableClasses({
708
- isFixed: true,
815
+ isFixed: false, // Use auto table-layout so columns size based on content
709
816
  variant,
710
817
  className: cn('border-collapse relative w-full', className)
711
818
  })}
@@ -800,7 +907,7 @@ function DataTableInternal<TData extends DataRecord>({
800
907
 
801
908
  {/* Column groups */}
802
909
  <colgroup>
803
- {hasSelectColumn && <col span={1} data-col-type="select" className="w-12" />}
910
+ {hasSelectColumn && <col span={1} data-col-type="select"/>}
804
911
  <col span={dataColumns} data-col-type="data" />
805
912
  {hasActionsColumn && <col span={1} data-col-type="actions"/>}
806
913
  </colgroup>
@@ -95,39 +95,40 @@ export function PaginationControls<TData extends DataRecord>({
95
95
  }
96
96
  };
97
97
 
98
-
99
98
  return (
100
99
  <footer
101
100
  aria-label="pagination"
102
- className="mx-auto grid grid-cols-[auto_auto_1fr_auto_auto_auto_auto] gap-4"
101
+ className="mx-auto grid grid-cols-[auto_auto_1fr_auto_auto_auto_auto] gap-4 items-center my-2"
103
102
  >
104
103
  {/* Left side - Page Size Selector */}
105
-
106
- <p className="text-sm text-sec-600">Rows per page</p>
107
- <Select
108
- value={currentPageSize?.toString() || '10'}
109
- selectedText={currentPageSize?.toString() || '10'}
110
- onValueChange={(value) => setPageSize(Number(value))}
111
- disabled={isLoading}
112
- className="w-36 h-8"
104
+
105
+ <label className="text-sec-600">Rows per page</label>
106
+ <Select
107
+ value={currentPageSize?.toString() || '10'}
108
+ selectedText={currentPageSize?.toString() || '10'}
109
+ onValueChange={(value) => setPageSize(Number(value))}
110
+ disabled={isLoading}
111
+ className="w-36 h-8"
112
+ >
113
+
114
+ <SelectTrigger
115
+ className={cn(
116
+
117
+ isLoading && "opacity-50 cursor-not-allowed"
118
+ )}
119
+ aria-label="Rows per page"
113
120
  >
114
- <SelectTrigger
115
- className={cn(
116
-
117
- isLoading && "opacity-50 cursor-not-allowed"
118
- )}
119
- aria-label="Rows per page"
120
- >
121
- <SelectValue />
122
- </SelectTrigger>
123
- <SelectContent>
124
- {availablePageSizes.map((pageSize) => (
125
- <SelectItem key={pageSize} value={pageSize?.toString() || '10'}>
126
- {pageSize}
127
- </SelectItem>
128
- ))}
129
- </SelectContent>
130
- </Select>
121
+ <SelectValue />
122
+ </SelectTrigger>
123
+ <SelectContent>
124
+ {availablePageSizes.map((pageSize) => (
125
+ <SelectItem key={pageSize} value={pageSize?.toString() || '10'}>
126
+ {pageSize}
127
+ </SelectItem>
128
+ ))}
129
+ </SelectContent>
130
+ </Select>
131
+
131
132
 
132
133
 
133
134
  {/* Performance Mode Indicator */}
@@ -144,61 +145,61 @@ export function PaginationControls<TData extends DataRecord>({
144
145
 
145
146
  {/* Center - Page Navigation */}
146
147
 
147
- <p className="justify-self-center text-sm text-sec-600">
148
- Page {currentPageIndex + 1} of {pageCount || 1}
149
- </p>
148
+ <p className="justify-self-center text-sm text-sec-600 my-0 py-0">
149
+ Page {currentPageIndex + 1} of {pageCount || 1}
150
+ </p>
150
151
 
151
152
 
152
153
  {/* Right side - Navigation Buttons */}
153
154
 
154
155
 
155
- <Button
156
- variant="outline"
157
- size="sm"
158
- className="h-8 w-8 p-0"
159
- onClick={goToFirstPage}
160
- disabled={!canPreviousPage || isLoading}
161
- aria-label="Go to first page"
162
- tabIndex={0}
163
- >
164
- <ChevronsLeft className="h-4 w-4" />
165
- </Button>
166
-
167
- <Button
168
- variant="outline"
169
- size="sm"
170
- className="h-8 w-8 p-0"
171
- onClick={goToPreviousPage}
172
- disabled={!canPreviousPage || isLoading}
173
- aria-label="Go to previous page"
174
- tabIndex={0}
175
- >
176
- <ChevronLeft className="h-4 w-4" />
177
- </Button>
178
-
179
- <Button
180
- variant="outline"
181
- size="sm"
182
- className="h-8 w-8 p-0"
183
- onClick={goToNextPage}
184
- disabled={!canNextPage || isLoading}
185
- aria-label="Go to next page"
186
- tabIndex={0}
187
- >
188
- <ChevronRight className="h-4 w-4" />
189
- </Button>
190
-
191
- <Button
192
- variant="outline"
193
- size="sm"
194
- className="h-8 w-8 p-0"
195
- onClick={goToLastPage}
196
- disabled={!canNextPage || isLoading}
197
- aria-label="Go to last page"
198
- tabIndex={0}
199
- >
200
- <ChevronsRight className="h-4 w-4" />
201
- </Button>
156
+ <Button
157
+ variant="outline"
158
+ size="sm"
159
+ className="h-8 w-8 p-0"
160
+ onClick={goToFirstPage}
161
+ disabled={!canPreviousPage || isLoading}
162
+ aria-label="Go to first page"
163
+ tabIndex={0}
164
+ >
165
+ <ChevronsLeft className="h-4 w-4" />
166
+ </Button>
167
+
168
+ <Button
169
+ variant="outline"
170
+ size="sm"
171
+ className="h-8 w-8 p-0"
172
+ onClick={goToPreviousPage}
173
+ disabled={!canPreviousPage || isLoading}
174
+ aria-label="Go to previous page"
175
+ tabIndex={0}
176
+ >
177
+ <ChevronLeft className="h-4 w-4" />
178
+ </Button>
179
+
180
+ <Button
181
+ variant="outline"
182
+ size="sm"
183
+ className="h-8 w-8 p-0"
184
+ onClick={goToNextPage}
185
+ disabled={!canNextPage || isLoading}
186
+ aria-label="Go to next page"
187
+ tabIndex={0}
188
+ >
189
+ <ChevronRight className="h-4 w-4" />
190
+ </Button>
191
+
192
+ <Button
193
+ variant="outline"
194
+ size="sm"
195
+ className="h-8 w-8 p-0"
196
+ onClick={goToLastPage}
197
+ disabled={!canNextPage || isLoading}
198
+ aria-label="Go to last page"
199
+ tabIndex={0}
200
+ >
201
+ <ChevronsRight className="h-4 w-4" />
202
+ </Button>
202
203
  </footer>
203
204
  );
204
205
  }
@@ -425,10 +425,18 @@ const RowComponent = React.memo(({
425
425
  );
426
426
  }
427
427
 
428
- // Skip leaf rows when grouping is enabled
429
- if (grouping.length > 0 && typeof row.getParentRow === 'function' && row.getParentRow() && (!row.getIsGrouped || !row.getIsGrouped())) {
430
- return null;
431
- }
428
+ // CRITICAL FIX: Removed the buggy filter that was hiding all rows when grouping is enabled
429
+ // TanStack Table's getRowModel() already correctly handles collapsing/expanding groups,
430
+ // returning only visible rows (group headers + children of expanded groups).
431
+ // The previous check was incorrectly filtering out all child rows regardless of parent expansion state,
432
+ // causing the bug where DataTable showed record count but no rows for large datasets.
433
+ //
434
+ // When grouping is enabled:
435
+ // - Group headers are rendered above (lines 317-426)
436
+ // - Child rows are ONLY in getRowModel().rows if their parent is expanded
437
+ // - Collapsed groups' children are NOT in getRowModel().rows (handled by TanStack Table)
438
+ //
439
+ // Therefore, we should trust getRowModel() and render all rows it returns.
432
440
 
433
441
  // If we're in edit mode, use EditableRow for better UX (auto-focus, keyboard shortcuts)
434
442
  if (isEditing && editingData && onEditingDataChange && onSaveEditing && onCancelEditing) {
@@ -40,20 +40,32 @@ export function useDataTableConfiguration<TData extends DataRecord>({
40
40
  finalDataCount,
41
41
  pageSize,
42
42
  }: UseDataTableConfigurationOptions<TData>) {
43
- return useMemo(() => ({
44
- data,
45
- columns,
46
- state: {
47
- sorting: stateSnapshot.sorting,
48
- columnFilters: stateSnapshot.columnFilters,
49
- columnVisibility: stateSnapshot.columnVisibility,
50
- rowSelection: stateSnapshot.rowSelection,
51
- grouping: stateSnapshot.grouping,
52
- expanded: stateSnapshot.expanded,
53
- pagination: stateSnapshot.pagination,
54
- globalFilter: stateSnapshot.globalFilter,
55
- columnOrder: stateSnapshot.columnOrder,
56
- },
43
+ return useMemo(() => {
44
+ // Normalize columnOrder: ensure 'select' is always first if selection is enabled
45
+ // This is critical for the UI - the select checkbox column must appear first
46
+ let normalizedColumnOrder = [...stateSnapshot.columnOrder];
47
+
48
+ if (features.selection) {
49
+ // Remove 'select' from wherever it might be
50
+ normalizedColumnOrder = normalizedColumnOrder.filter(id => id !== 'select');
51
+ // Always place 'select' first
52
+ normalizedColumnOrder = ['select', ...normalizedColumnOrder];
53
+ }
54
+
55
+ return {
56
+ data,
57
+ columns,
58
+ state: {
59
+ sorting: stateSnapshot.sorting,
60
+ columnFilters: stateSnapshot.columnFilters,
61
+ columnVisibility: stateSnapshot.columnVisibility,
62
+ rowSelection: stateSnapshot.rowSelection,
63
+ grouping: stateSnapshot.grouping,
64
+ expanded: stateSnapshot.expanded,
65
+ pagination: stateSnapshot.pagination,
66
+ globalFilter: stateSnapshot.globalFilter,
67
+ columnOrder: normalizedColumnOrder,
68
+ },
57
69
  initialState: {
58
70
  expanded: features.grouping ? {} : undefined,
59
71
  },
@@ -70,10 +82,11 @@ export function useDataTableConfiguration<TData extends DataRecord>({
70
82
  manualSorting: finalPaginationMode === 'server',
71
83
  manualFiltering: finalPaginationMode === 'server',
72
84
  manualPagination: finalPaginationMode === 'server',
73
- pageCount: finalPaginationMode === 'server'
85
+ pageCount: finalPaginationMode === 'server'
74
86
  ? Math.ceil(finalDataCount / pageSize)
75
87
  : undefined,
76
- }), [
88
+ };
89
+ }, [
77
90
  data,
78
91
  columns,
79
92
  stateSnapshot,
@@ -24,10 +24,12 @@ export function useEffectiveColumnOrder<TData extends DataRecord>({
24
24
  }, [columns, externalColumnOrder]);
25
25
 
26
26
  return useMemo(() => {
27
- if (selectionEnabled && !baseOrder.includes('select')) {
28
- return ['select', ...baseOrder];
27
+ if (!selectionEnabled) {
28
+ return baseOrder;
29
29
  }
30
-
31
- return baseOrder;
30
+
31
+ // Always ensure 'select' is first, even if it appears elsewhere in the order
32
+ const orderWithoutSelect = baseOrder.filter(id => id !== 'select');
33
+ return ['select', ...orderWithoutSelect];
32
34
  }, [baseOrder, selectionEnabled]);
33
35
  }
@@ -95,24 +95,28 @@ export function useTableColumns<TData extends DataRecord>({
95
95
  meta: { align: 'right' },
96
96
  }) : null;
97
97
 
98
- // Build final columns array respecting columnOrder
98
+ // Build final columns array - selection ALWAYS first, actions ALWAYS last
99
99
  const finalColumns: ColumnDef<TData>[] = [];
100
100
 
101
+ // ALWAYS add selection column first if it exists (regardless of columnOrder)
102
+ if (selectionColumn) {
103
+ finalColumns.push(selectionColumn);
104
+ }
105
+
101
106
  if (columnOrder && columnOrder.length > 0) {
102
- // Create set of used column IDs for remaining columns check
103
- const usedColumnIds = new Set(columnOrder);
104
-
105
- // Add selection column first if it wasn't in the order
106
- if (selectionColumn && !usedColumnIds.has('select')) {
107
- finalColumns.unshift(selectionColumn);
108
- }
107
+ // Create set of used column IDs (excluding 'select' and 'actions' which are handled separately)
108
+ const usedColumnIds = new Set(
109
+ columnOrder.filter(id => id !== 'select' && id !== 'actions')
110
+ );
109
111
 
110
- // Use provided columnOrder to arrange columns
112
+ // Process columnOrder, skipping 'select' and 'actions' (handled separately)
111
113
  for (const columnId of columnOrder) {
112
- if (columnId === 'select' && selectionColumn) {
113
- finalColumns.push(selectionColumn);
114
+ if (columnId === 'select') {
115
+ // Skip - already added first
116
+ continue;
114
117
  } else if (columnId === 'actions' && actionsColumn) {
115
- finalColumns.push(actionsColumn);
118
+ // Will be added at the end
119
+ continue;
116
120
  } else {
117
121
  // Find the data column by id or accessorKey
118
122
  const dataColumn = baseColumns.find(col =>
@@ -124,22 +128,19 @@ export function useTableColumns<TData extends DataRecord>({
124
128
  }
125
129
  }
126
130
 
127
- // Add any remaining columns that weren't in the columnOrder
131
+ // Add any remaining data columns that weren't in the columnOrder
128
132
  const remainingDataColumns = baseColumns.filter(col => {
129
133
  const colId = col.id ? String(col.id) : ('accessorKey' in col ? String(col.accessorKey) : '');
130
134
  return !usedColumnIds.has(colId);
131
135
  });
132
136
  finalColumns.push(...remainingDataColumns);
133
137
 
134
- // Add actions column if it wasn't in the order
135
- if (actionsColumn && !usedColumnIds.has('actions')) {
138
+ // Add actions column last if it exists (regardless of columnOrder position)
139
+ if (actionsColumn) {
136
140
  finalColumns.push(actionsColumn);
137
141
  }
138
142
  } else {
139
- // No columnOrder provided, use default behavior
140
- if (selectionColumn) {
141
- finalColumns.push(selectionColumn);
142
- }
143
+ // No columnOrder provided: selection (already added), then data columns, then actions
143
144
  finalColumns.push(...baseColumns);
144
145
  if (actionsColumn) {
145
146
  finalColumns.push(actionsColumn);
@@ -139,15 +139,20 @@ export function useTableHandlers<TData extends DataRecord>({
139
139
  const nextValue = typeof updaterOrValue === 'function'
140
140
  ? (updaterOrValue as (prev: string[]) => string[])(stateSnapshot.columnOrder)
141
141
  : updaterOrValue as string[];
142
+
143
+ // Normalize: ensure 'select' stays first if it exists
144
+ const normalizedOrder = nextValue.includes('select')
145
+ ? ['select', ...nextValue.filter(id => id !== 'select')]
146
+ : nextValue;
142
147
 
143
- actions.setColumnOrder(nextValue);
148
+ actions.setColumnOrder(normalizedOrder);
144
149
 
145
150
  if (canPersistOrder) {
146
- updateColumnOrder(nextValue);
151
+ updateColumnOrder(normalizedOrder);
147
152
  }
148
153
 
149
154
  onLayoutChange?.({
150
- columnOrder: nextValue,
155
+ columnOrder: normalizedOrder,
151
156
  columnVisibility: stateSnapshot.columnVisibility,
152
157
  });
153
158
  }, [actions, stateSnapshot.columnOrder, stateSnapshot.columnVisibility, canPersistOrder, updateColumnOrder, onLayoutChange]);
@@ -16,7 +16,7 @@ import { cn } from '../../utils/cn';
16
16
 
17
17
  export const tableStyles = {
18
18
  // Main table container
19
- table: 'w-full caption-bottom text-sm',
19
+ table: 'w-full caption-top text-sm',
20
20
  tableFixed: 'w-full table-fixed',
21
21
 
22
22
  // Table sections