react-live-data-table 1.0.19 → 1.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-live-data-table",
3
- "version": "1.0.19",
3
+ "version": "1.1.1",
4
4
  "description": "Your React component package with Tailwind",
5
5
  "main": "src/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -21,6 +21,10 @@ function ReactDataTable({
21
21
  rowStyle = {},
22
22
  rowClassName = "",
23
23
  columnReorder = false,
24
+ subTableColumns = null, // Array of column definitions for subtable
25
+ expandedRows = {},
26
+ onRowExpand = null,
27
+ subTableWidth = "100%",
24
28
  }) {
25
29
  const tableContainerRef = React.useRef(null);
26
30
  const [data, setData] = React.useState({ pages: [], meta: { totalPages: 1 } });
@@ -33,17 +37,103 @@ function ReactDataTable({
33
37
  const [startX, setStartX] = useState(null);
34
38
  const [initialWidth, setInitialWidth] = useState(null);
35
39
  const [tableWidth, setTableWidth] = useState(0);
36
-
40
+
37
41
  // Column reordering state - only used when columnReorder is true
38
42
  const [orderedColumns, setOrderedColumns] = useState(columns);
39
43
  const [draggedColumn, setDraggedColumn] = useState(null);
40
44
  const [dragOverColumn, setDragOverColumn] = useState(null);
41
-
42
- // Ref to store column widths to ensure persistence during data loading
43
- const persistedColumnWidthsRef = React.useRef([]);
44
45
 
46
+ const persistedColumnWidthsRef = React.useRef([]);
45
47
  const flatData = data.pages.flatMap(page => page.data);
46
48
 
49
+ // Helper function to check if a row has subtable data
50
+ const hasSubTableData = useCallback((row) => {
51
+ if (!subTableColumns) return false;
52
+
53
+ return subTableColumns.some(col => {
54
+ const value = row[col.accessorKey || col.id];
55
+ // Check for arrays with content OR non-empty strings
56
+ return (Array.isArray(value) && value.length > 0) ||
57
+ (typeof value === 'string' && value.trim() !== '' && value !== '--');
58
+ });
59
+ }, [subTableColumns]);
60
+
61
+ // Helper function to get subtable rows from main row data
62
+ const getSubTableRows = useCallback((row) => {
63
+ if (!subTableColumns || !hasSubTableData(row)) return [];
64
+
65
+ // Find the maximum length among all array columns, default to 1 for strings
66
+ let maxLength = 1; // At least 1 row for string values
67
+ subTableColumns.forEach(col => {
68
+ const value = row[col.accessorKey || col.id];
69
+ if (Array.isArray(value)) {
70
+ maxLength = Math.max(maxLength, value.length);
71
+ }
72
+ });
73
+
74
+ // Create rows for subtable
75
+ const subRows = [];
76
+ for (let i = 0; i < maxLength; i++) {
77
+ const subRow = { id: `${row.id}-sub-${i}` };
78
+
79
+ subTableColumns.forEach(col => {
80
+ const value = row[col.accessorKey || col.id];
81
+ if (Array.isArray(value)) {
82
+ // For arrays, use the value at index i, or undefined if out of bounds
83
+ subRow[col.accessorKey || col.id] = i < value.length ? value[i] : undefined;
84
+ } else {
85
+ // For strings/non-arrays, use the value only in the first row
86
+ subRow[col.accessorKey || col.id] = i === 0 ? value : undefined;
87
+ }
88
+ });
89
+
90
+ subRows.push(subRow);
91
+ }
92
+
93
+ return subRows;
94
+ }, [subTableColumns, hasSubTableData]);
95
+
96
+ const calculateDynamicHeight = useCallback(() => {
97
+ const expandedCount = Object.values(expandedRows || {}).filter(Boolean).length;
98
+
99
+ if (expandedCount === 0) {
100
+ return height;
101
+ }
102
+
103
+ // Calculate subtable height based on actual data
104
+ let totalSubTableHeight = 0;
105
+ Object.keys(expandedRows || {}).forEach(rowId => {
106
+ if (expandedRows[rowId]) {
107
+ const row = flatData.find(r => r.id === rowId);
108
+ if (row) {
109
+ const subRows = getSubTableRows(row);
110
+ const headerHeight = 36;
111
+ const rowsHeight = subRows.length * 32; // Assuming 32px per subtable row
112
+ totalSubTableHeight += headerHeight + rowsHeight + 20; // +20 for padding
113
+ }
114
+ }
115
+ });
116
+
117
+ const baseHeight = typeof height === 'string' ? parseInt(height) : height;
118
+ return `${Math.max(baseHeight, baseHeight + totalSubTableHeight)}px`;
119
+ }, [height, expandedRows, flatData, getSubTableRows]);
120
+
121
+ const dynamicHeight = calculateDynamicHeight();
122
+
123
+ const handleRowExpand = useCallback((rowId) => {
124
+ const currentExpandedRows = expandedRows || {};
125
+ const isCurrentlyExpanded = currentExpandedRows[rowId];
126
+
127
+ if (isCurrentlyExpanded) {
128
+ const newExpandedRows = { ...currentExpandedRows };
129
+ delete newExpandedRows[rowId];
130
+ onRowExpand?.(newExpandedRows, rowId);
131
+ } else {
132
+ const newExpandedRows = { [rowId]: true };
133
+ onRowExpand?.(newExpandedRows, rowId);
134
+ }
135
+ }, [expandedRows, onRowExpand]);
136
+
47
137
  const checkboxColumn = {
48
138
  id: 'select',
49
139
  size: 50,
@@ -113,9 +203,59 @@ function ReactDataTable({
113
203
  }
114
204
  };
115
205
 
116
- const enhancedColumns = showCheckbox ? [checkboxColumn, ...orderedColumns] : orderedColumns;
206
+ // Expand/collapse column for sub-tables - keep it similar to checkbox column
207
+ const expandColumn = {
208
+ id: 'expand',
209
+ size: 50,
210
+ minWidth: 50,
211
+ resizable: false,
212
+ reorderable: false,
213
+ textAlign: "center",
214
+ header: () => (
215
+ <div className="flex items-center justify-center h-[40px]">
216
+ <span className="text-xs text-gray-600">Expand</span>
217
+ </div>
218
+ ),
219
+ cell: ({ row }) => {
220
+ const isExpanded = (expandedRows || {})[row.id];
221
+ const hasSubData = hasSubTableData(row);
222
+
223
+ if (!hasSubData) {
224
+ return <div className="flex items-center justify-center h-[40px]"></div>;
225
+ }
226
+
227
+ return (
228
+ <div className="flex items-center justify-center h-[40px]" onClick={(e) => e.stopPropagation()}>
229
+ <button
230
+ onClick={(e) => {
231
+ e.stopPropagation();
232
+ handleRowExpand(row.id);
233
+ }}
234
+ className="w-6 h-6 flex items-center justify-center rounded hover:bg-gray-200 transition-colors"
235
+ >
236
+ <svg
237
+ className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
238
+ fill="none"
239
+ stroke="currentColor"
240
+ viewBox="0 0 24 24"
241
+ >
242
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
243
+ </svg>
244
+ </button>
245
+ </div>
246
+ );
247
+ }
248
+ };
249
+
250
+ const hasAnySubTableData = subTableColumns && flatData.some(row => hasSubTableData(row));
251
+
252
+ const enhancedColumns = (() => {
253
+ let cols = [];
254
+ if (showCheckbox) cols.push(checkboxColumn);
255
+ if (hasAnySubTableData) cols.push(expandColumn);
256
+ return [...cols, ...orderedColumns];
257
+ })();
117
258
 
118
- // Update ordered columns when columns prop changes
119
259
  useEffect(() => {
120
260
  setOrderedColumns(columns);
121
261
  }, [columns]);
@@ -127,10 +267,10 @@ function ReactDataTable({
127
267
  }
128
268
  }, [selected]);
129
269
 
130
- // Initialize column widths array only once when component mounts or columns change
270
+ // Initialize column widths array - FIXED: Added enhancedColumns dependency
131
271
  useEffect(() => {
132
- const allColumns = showCheckbox ? [{ id: 'select', size: 50 }, ...orderedColumns] : orderedColumns;
133
-
272
+ const allColumns = enhancedColumns;
273
+
134
274
  // If we have persisted widths and the number of columns matches, use those
135
275
  if (persistedColumnWidthsRef.current.length === allColumns.length) {
136
276
  setColumnWidths([...persistedColumnWidthsRef.current]);
@@ -143,7 +283,7 @@ function ReactDataTable({
143
283
  // Store in our ref for persistence
144
284
  persistedColumnWidthsRef.current = [...initialWidths];
145
285
  }
146
- }, [orderedColumns, showCheckbox]);
286
+ }, [showCheckbox]); // FIXED: Added enhancedColumns dependency
147
287
 
148
288
  useEffect(() => {
149
289
  setData({ pages: [], meta: { totalPages: 1 } });
@@ -186,7 +326,7 @@ function ReactDataTable({
186
326
 
187
327
  const handleDrop = (e, dropIndex) => {
188
328
  e.preventDefault();
189
-
329
+
190
330
  if (!columnReorder || draggedColumn === null || draggedColumn === dropIndex) {
191
331
  setDraggedColumn(null);
192
332
  setDragOverColumn(null);
@@ -201,12 +341,20 @@ function ReactDataTable({
201
341
 
202
342
  const newColumns = [...enhancedColumns];
203
343
  const draggedColumnData = newColumns[draggedColumn];
204
-
344
+
205
345
  newColumns.splice(draggedColumn, 1);
206
346
  const adjustedDropIndex = draggedColumn < dropIndex ? dropIndex - 1 : dropIndex;
207
347
  newColumns.splice(adjustedDropIndex, 0, draggedColumnData);
208
348
 
209
- const updatedOrderedColumns = showCheckbox ? newColumns.slice(1) : newColumns;
349
+ // FIXED: Updated logic to handle different column types
350
+ let updatedOrderedColumns;
351
+ if (showCheckbox && hasAnySubTableData) {
352
+ updatedOrderedColumns = newColumns.slice(2); // Remove both checkbox and expand columns
353
+ } else if (showCheckbox || hasAnySubTableData) {
354
+ updatedOrderedColumns = newColumns.slice(1); // Remove one column (checkbox or expand)
355
+ } else {
356
+ updatedOrderedColumns = newColumns;
357
+ }
210
358
  setOrderedColumns(updatedOrderedColumns);
211
359
 
212
360
  // Reorder column widths
@@ -217,8 +365,6 @@ function ReactDataTable({
217
365
  setColumnWidths(newColumnWidths);
218
366
  persistedColumnWidthsRef.current = newColumnWidths;
219
367
 
220
- // onColumnReorder?.(updatedOrderedColumns);
221
-
222
368
  setDraggedColumn(null);
223
369
  setDragOverColumn(null);
224
370
  };
@@ -229,17 +375,12 @@ function ReactDataTable({
229
375
  const delta = e.clientX - startX;
230
376
  const newWidth = Math.max(enhancedColumns[resizingIndex]?.minWidth || 80, initialWidth + delta);
231
377
 
232
- // Create a new array of column widths with the updated width
233
378
  const newColumnWidths = [...columnWidths];
234
379
  newColumnWidths[resizingIndex] = newWidth;
235
380
 
236
- // Update the column widths state
237
381
  setColumnWidths(newColumnWidths);
238
-
239
- // Update our persisted ref to maintain widths during pagination
240
382
  persistedColumnWidthsRef.current = newColumnWidths;
241
383
 
242
- // Recalculate table width based on the new column widths
243
384
  const newTableWidth = newColumnWidths.reduce((sum, width) => sum + width, 0);
244
385
  setTableWidth(newTableWidth);
245
386
  }, [resizingIndex, startX, initialWidth, enhancedColumns, columnWidths]);
@@ -250,7 +391,6 @@ function ReactDataTable({
250
391
  setInitialWidth(null);
251
392
  }, []);
252
393
 
253
- // Add resize event listeners
254
394
  useEffect(() => {
255
395
  if (resizingIndex !== null) {
256
396
  document.addEventListener('mousemove', handleMouseMove);
@@ -299,7 +439,6 @@ function ReactDataTable({
299
439
  limit: defaultLimit
300
440
  });
301
441
 
302
- // Update the data but maintain our column widths
303
442
  setData(prev => ({
304
443
  pages: [...prev.pages, nextData],
305
444
  meta: nextData.meta
@@ -423,7 +562,7 @@ function ReactDataTable({
423
562
 
424
563
  {
425
564
  flatData.length === 0 && !loading ? (
426
- <div className="flex items-center justify-center" style={{ height }}>
565
+ <div className="flex items-center justify-center" style={{ height: dynamicHeight }}>
427
566
  <div className="text-gray-500">
428
567
  {emptyText || 'No data available'}
429
568
  </div>
@@ -433,13 +572,18 @@ function ReactDataTable({
433
572
  <div
434
573
  ref={tableContainerRef}
435
574
  className="overflow-auto w-full"
436
- style={{ maxHeight, height }}
575
+ style={{
576
+ maxHeight: dynamicHeight,
577
+ height: dynamicHeight
578
+ }}
437
579
  onScroll={(e) => handleScroll(e.currentTarget)}
438
580
  >
439
581
  <table
440
582
  className="w-full border-collapse"
441
583
  style={{
442
584
  tableLayout: 'fixed',
585
+ width: `${tableWidth}px`, // FIXED: Use calculated table width for scrolling
586
+ minWidth: '100%'
443
587
  }}
444
588
  >
445
589
  <thead
@@ -455,16 +599,14 @@ function ReactDataTable({
455
599
  return (
456
600
  <th
457
601
  key={column.accessorKey || column.id}
458
- className={`text-left font-normal h-[40px] border-b border-t border-solid border-[#e4e3e2] relative select-none ${
459
- columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
460
- } ${isReorderable ? 'cursor-move' : ''} ${
461
- isDropTarget ? 'bg-blue-400' : ''
462
- }`}
602
+ className={`text-left font-normal h-[40px] border-b border-t border-solid border-[#e4e3e2] relative select-none ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
603
+ } ${isReorderable ? 'cursor-move' : ''} ${isDropTarget ? 'bg-blue-400' : ''
604
+ }`}
463
605
  style={{
464
606
  width: `${width}px`,
465
607
  minWidth: `${width}px`,
466
608
  maxWidth: `${width}px`,
467
- textAlign: column.textAlign,
609
+ textAlign: column.textAlign || "left",
468
610
  }}
469
611
  draggable={isReorderable}
470
612
  onDragStart={(e) => handleDragStart(e, columnIndex)}
@@ -480,9 +622,8 @@ function ReactDataTable({
480
622
  {/* Resize handle - Only show if column is resizable */}
481
623
  {column.resizable !== false && (
482
624
  <div
483
- className={`absolute top-0 right-0 h-full w-4 flex items-center justify-center group ${
484
- resizingIndex === columnIndex ? 'bg-blue-100' : 'hover:bg-blue-100'
485
- } transition-colors duration-200`}
625
+ className={`absolute top-0 right-0 h-full w-4 flex items-center justify-center group ${resizingIndex === columnIndex ? 'bg-blue-100' : 'hover:bg-blue-100'
626
+ } transition-colors duration-200`}
486
627
  onMouseDown={(e) => handleResizeStart(e, columnIndex)}
487
628
  style={{
488
629
  touchAction: 'none',
@@ -501,41 +642,128 @@ function ReactDataTable({
501
642
  {flatData.length > 0 ? (
502
643
  flatData.map((row, rowIndex) => {
503
644
  const isLastRow = rowIndex === flatData.length - 1;
645
+ const isExpanded = (expandedRows || {})[row.id];
646
+ const subRows = getSubTableRows(row);
647
+
504
648
  return (
505
- <tr
506
- key={row.id}
507
- className={`react-live-data-table-row-${rowIndex} border-t ${isLastRow ? 'border-b' : ''} border-gray-200 hover:bg-[#dee1f2] ${selectedRows[row.id] ? 'bg-[#dee1f2]' : ''} ${rowClassName} cursor-pointer`}
508
- style={{
509
- height: `${rowHeights}px`,
510
- ...rowStyle,
511
- ...(typeof rowStyle === 'function' ? rowStyle(row, rowIndex) : {})
512
- }}
513
- onClick={() => handleRowClick(row, rowIndex, flatData)}
514
- >
515
- {enhancedColumns.map((column, columnIndex) => {
516
- const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
517
-
518
- return (
519
- <td
520
- key={column.accessorKey || column.id}
521
- className={`text-left font-normal ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
522
- } ${column?.cellProps?.className || ''}`}
523
- style={{
524
- width: `${width}px`,
525
- minWidth: `${width}px`,
526
- maxWidth: `${width}px`,
527
- textAlign: column?.textAlign,
528
- ...column?.cellProps?.style,
529
- overflow: 'hidden',
530
- textOverflow: 'ellipsis',
531
- whiteSpace: 'nowrap'
532
- }}
533
- >
534
- {typeof column.cell === 'function' ? column.cell({ row }) : null}
649
+ <React.Fragment key={row.id}>
650
+ <tr
651
+ className={`react-live-data-table-row-${rowIndex} border-t ${isLastRow ? 'border-b' : ''} border-gray-200 hover:bg-[#dee1f2] ${selectedRows[row.id] ? 'bg-[#dee1f2]' : ''} ${rowClassName} cursor-pointer`}
652
+ style={{
653
+ height: `${rowHeights}px`,
654
+ ...rowStyle,
655
+ ...(typeof rowStyle === 'function' ? rowStyle(row, rowIndex) : {})
656
+ }}
657
+ onClick={() => handleRowClick(row, rowIndex, flatData)}
658
+ >
659
+ {enhancedColumns.map((column, columnIndex) => {
660
+ const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
661
+
662
+ return (
663
+ <td
664
+ key={column.accessorKey || column.id}
665
+ className={`text-left font-normal ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
666
+ } ${column?.cellProps?.className || ''}`}
667
+ style={{
668
+ width: `${width}px`,
669
+ minWidth: `${width}px`,
670
+ maxWidth: `${width}px`,
671
+ textAlign: column.textAlign || "left",
672
+ ...column?.cellProps?.style,
673
+ overflow: 'hidden',
674
+ textOverflow: 'ellipsis',
675
+ whiteSpace: 'nowrap'
676
+ }}
677
+ >
678
+ {typeof column.cell === 'function' ? column.cell({ row }) : null}
679
+ </td>
680
+ );
681
+ })}
682
+ </tr>
683
+ {/* Subtable row */}
684
+ {isExpanded && subRows.length > 0 && (
685
+ <tr className="bg-gray-50 border-b border-gray-200">
686
+ <td colSpan={enhancedColumns.length} className="p-0">
687
+ <div className="bg-white border border-gray-200" style={{ width: subTableWidth, maxWidth: '100%' }}>
688
+ <div
689
+ className="sub-table-container"
690
+ style={{
691
+ maxHeight: "300px",
692
+ width: '100%',
693
+ overflow: 'auto',
694
+ scrollbarWidth: 'thin',
695
+ scrollbarColor: '#cbd5e0 #f7fafc',
696
+ }}
697
+ >
698
+ <style>
699
+ {`
700
+ .sub-table-container::-webkit-scrollbar {
701
+ width: 6px;
702
+ height: 6px;
703
+ }
704
+ .sub-table-container::-webkit-scrollbar-track {
705
+ background: #f7fafc;
706
+ border-radius: 3px;
707
+ }
708
+ .sub-table-container::-webkit-scrollbar-thumb {
709
+ background: #cbd5e0;
710
+ border-radius: 3px;
711
+ }
712
+ .sub-table-container::-webkit-scrollbar-thumb:hover {
713
+ background: #a0aec0;
714
+ }
715
+ `}
716
+ </style>
717
+ <table className="w-full" style={{ tableLayout: 'fixed' }}>
718
+ <thead className="bg-gray-100 sticky top-0 z-10">
719
+ <tr>
720
+ {subTableColumns.map((col, colIndex) => {
721
+ return (
722
+ <th
723
+ key={col.id || col.accessorKey || colIndex}
724
+ className={`px-4 py-2 text-left text-sm font-medium text-black border-b border-gray-200 ${colIndex < subTableColumns.length - 1 ? 'border-r border-gray-200' : ''
725
+ }`}
726
+ style={{
727
+ width: col.size ? `${col.size}px` : "auto",
728
+ minWidth: col.minWidth ? `${col.minWidth}px` : "80px",
729
+ }}
730
+ >
731
+ {typeof col.header === 'function' ? col.header() : (col.header || col.name)}
732
+ </th>
733
+
734
+ );
735
+ })}
736
+ </tr>
737
+ </thead>
738
+ <tbody>
739
+ {subRows.map((subRow, subRowIndex) => (
740
+ <tr key={subRow.id} className="border-b border-gray-100 hover:bg-gray-50">
741
+ {subTableColumns.map((col, colIndex) => {
742
+ return (
743
+ <td
744
+ key={col.id || col.accessorKey || colIndex}
745
+ className={`px-4 py-2 text-sm text-gray-900 ${colIndex < subTableColumns.length - 1 ? 'border-r border-gray-200' : ''
746
+ }`}
747
+ style={{
748
+ width: col.size ? `${col.size}px` : "auto",
749
+ minWidth: col.minWidth ? `${col.minWidth}px` : "80px",
750
+ }}
751
+ >
752
+ {col.cell ? col.cell({ row: subRow }) : subRow[col.accessorKey || col.id]}
753
+ </td>
754
+
755
+ );
756
+ })}
757
+ </tr>
758
+ ))}
759
+ </tbody>
760
+ </table>
761
+ </div>
762
+ </div>
535
763
  </td>
536
- );
537
- })}
538
- </tr>
764
+ </tr>
765
+ )}
766
+ </React.Fragment>
539
767
  );
540
768
  })
541
769
  ) : (