@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.
- package/dist/{DataTable-BCBW5SCL.js → DataTable-OBT663FS.js} +6 -6
- package/dist/{UnifiedAuthProvider-TSHK77PL.js → UnifiedAuthProvider-K2IZAY5F.js} +3 -3
- package/dist/{chunk-TI67X46U.js → chunk-3FV24IOD.js} +7 -7
- package/dist/{chunk-WYKXRCXB.js → chunk-5BN3YGNK.js} +157 -53
- package/dist/{chunk-WYKXRCXB.js.map → chunk-5BN3YGNK.js.map} +1 -1
- package/dist/{chunk-RMK6FOHF.js → chunk-CACHCRZS.js} +158 -79
- package/dist/{chunk-RMK6FOHF.js.map → chunk-CACHCRZS.js.map} +1 -1
- package/dist/{chunk-GXWREXH7.js → chunk-CBSD3BZ3.js} +2 -2
- package/dist/{chunk-CALYF6HH.js → chunk-I2VVV5PQ.js} +2 -2
- package/dist/{chunk-3WFKFBVQ.js → chunk-KUYWZVR2.js} +4 -4
- package/dist/{chunk-LVV6J6ZF.js → chunk-NTW3KGS4.js} +5 -5
- package/dist/{chunk-HQ7KTKC3.js → chunk-RIXPZJUB.js} +2 -2
- package/dist/{chunk-JYCP4L55.js → chunk-S3JKDMD5.js} +3 -3
- package/dist/{chunk-OBXLAL3J.js → chunk-V5SWX6KL.js} +4 -4
- package/dist/{chunk-IQFITAE3.js → chunk-YVUZWLQG.js} +3 -3
- package/dist/components.js +8 -8
- package/dist/hooks.js +7 -7
- package/dist/index.js +11 -11
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/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/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/EventLogoProps.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/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/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/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.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/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/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/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 +3 -3
- package/docs/architecture/rpc-function-standards.md +757 -0
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/styles.test.ts +5 -5
- package/src/components/DataTable/components/DataTableCore.tsx +125 -18
- package/src/components/DataTable/components/PaginationControls.tsx +78 -77
- package/src/components/DataTable/components/UnifiedTableBody.tsx +12 -4
- package/src/components/DataTable/hooks/useDataTableConfiguration.ts +29 -16
- package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +6 -4
- package/src/components/DataTable/hooks/useTableColumns.ts +20 -19
- package/src/components/DataTable/hooks/useTableHandlers.ts +8 -3
- package/src/components/DataTable/styles.ts +1 -1
- package/src/providers/services/UnifiedAuthProvider.tsx +175 -54
- package/src/styles/core.css +1 -2
- package/src/styles/base.css +0 -208
- /package/dist/{DataTable-BCBW5SCL.js.map → DataTable-OBT663FS.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-TSHK77PL.js.map → UnifiedAuthProvider-K2IZAY5F.js.map} +0 -0
- /package/dist/{chunk-TI67X46U.js.map → chunk-3FV24IOD.js.map} +0 -0
- /package/dist/{chunk-GXWREXH7.js.map → chunk-CBSD3BZ3.js.map} +0 -0
- /package/dist/{chunk-CALYF6HH.js.map → chunk-I2VVV5PQ.js.map} +0 -0
- /package/dist/{chunk-3WFKFBVQ.js.map → chunk-KUYWZVR2.js.map} +0 -0
- /package/dist/{chunk-LVV6J6ZF.js.map → chunk-NTW3KGS4.js.map} +0 -0
- /package/dist/{chunk-HQ7KTKC3.js.map → chunk-RIXPZJUB.js.map} +0 -0
- /package/dist/{chunk-JYCP4L55.js.map → chunk-S3JKDMD5.js.map} +0 -0
- /package/dist/{chunk-OBXLAL3J.js.map → chunk-V5SWX6KL.js.map} +0 -0
- /package/dist/{chunk-IQFITAE3.js.map → chunk-YVUZWLQG.js.map} +0 -0
package/package.json
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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:
|
|
696
|
+
columnOrder: normalizedColumnOrderForColumns
|
|
616
697
|
});
|
|
617
698
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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"
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
return
|
|
27
|
+
if (!selectionEnabled) {
|
|
28
|
+
return baseOrder;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
|
103
|
-
const usedColumnIds = new Set(
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
//
|
|
112
|
+
// Process columnOrder, skipping 'select' and 'actions' (handled separately)
|
|
111
113
|
for (const columnId of columnOrder) {
|
|
112
|
-
if (columnId === 'select'
|
|
113
|
-
|
|
114
|
+
if (columnId === 'select') {
|
|
115
|
+
// Skip - already added first
|
|
116
|
+
continue;
|
|
114
117
|
} else if (columnId === 'actions' && actionsColumn) {
|
|
115
|
-
|
|
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
|
|
135
|
-
if (actionsColumn
|
|
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,
|
|
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(
|
|
148
|
+
actions.setColumnOrder(normalizedOrder);
|
|
144
149
|
|
|
145
150
|
if (canPersistOrder) {
|
|
146
|
-
updateColumnOrder(
|
|
151
|
+
updateColumnOrder(normalizedOrder);
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
onLayoutChange?.({
|
|
150
|
-
columnOrder:
|
|
155
|
+
columnOrder: normalizedOrder,
|
|
151
156
|
columnVisibility: stateSnapshot.columnVisibility,
|
|
152
157
|
});
|
|
153
158
|
}, [actions, stateSnapshot.columnOrder, stateSnapshot.columnVisibility, canPersistOrder, updateColumnOrder, onLayoutChange]);
|