react-live-data-table 1.0.19 → 1.1.2

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.2",
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,9 @@ function ReactDataTable({
127
267
  }
128
268
  }, [selected]);
129
269
 
130
- // Initialize column widths array only once when component mounts or columns change
131
270
  useEffect(() => {
132
- const allColumns = showCheckbox ? [{ id: 'select', size: 50 }, ...orderedColumns] : orderedColumns;
133
-
271
+ const allColumns = enhancedColumns;
272
+
134
273
  // If we have persisted widths and the number of columns matches, use those
135
274
  if (persistedColumnWidthsRef.current.length === allColumns.length) {
136
275
  setColumnWidths([...persistedColumnWidthsRef.current]);
@@ -143,7 +282,7 @@ function ReactDataTable({
143
282
  // Store in our ref for persistence
144
283
  persistedColumnWidthsRef.current = [...initialWidths];
145
284
  }
146
- }, [orderedColumns, showCheckbox]);
285
+ }, [showCheckbox, hasAnySubTableData]);
147
286
 
148
287
  useEffect(() => {
149
288
  setData({ pages: [], meta: { totalPages: 1 } });
@@ -186,7 +325,7 @@ function ReactDataTable({
186
325
 
187
326
  const handleDrop = (e, dropIndex) => {
188
327
  e.preventDefault();
189
-
328
+
190
329
  if (!columnReorder || draggedColumn === null || draggedColumn === dropIndex) {
191
330
  setDraggedColumn(null);
192
331
  setDragOverColumn(null);
@@ -201,12 +340,20 @@ function ReactDataTable({
201
340
 
202
341
  const newColumns = [...enhancedColumns];
203
342
  const draggedColumnData = newColumns[draggedColumn];
204
-
343
+
205
344
  newColumns.splice(draggedColumn, 1);
206
345
  const adjustedDropIndex = draggedColumn < dropIndex ? dropIndex - 1 : dropIndex;
207
346
  newColumns.splice(adjustedDropIndex, 0, draggedColumnData);
208
347
 
209
- const updatedOrderedColumns = showCheckbox ? newColumns.slice(1) : newColumns;
348
+ // FIXED: Updated logic to handle different column types
349
+ let updatedOrderedColumns;
350
+ if (showCheckbox && hasAnySubTableData) {
351
+ updatedOrderedColumns = newColumns.slice(2); // Remove both checkbox and expand columns
352
+ } else if (showCheckbox || hasAnySubTableData) {
353
+ updatedOrderedColumns = newColumns.slice(1); // Remove one column (checkbox or expand)
354
+ } else {
355
+ updatedOrderedColumns = newColumns;
356
+ }
210
357
  setOrderedColumns(updatedOrderedColumns);
211
358
 
212
359
  // Reorder column widths
@@ -217,8 +364,6 @@ function ReactDataTable({
217
364
  setColumnWidths(newColumnWidths);
218
365
  persistedColumnWidthsRef.current = newColumnWidths;
219
366
 
220
- // onColumnReorder?.(updatedOrderedColumns);
221
-
222
367
  setDraggedColumn(null);
223
368
  setDragOverColumn(null);
224
369
  };
@@ -229,17 +374,12 @@ function ReactDataTable({
229
374
  const delta = e.clientX - startX;
230
375
  const newWidth = Math.max(enhancedColumns[resizingIndex]?.minWidth || 80, initialWidth + delta);
231
376
 
232
- // Create a new array of column widths with the updated width
233
377
  const newColumnWidths = [...columnWidths];
234
378
  newColumnWidths[resizingIndex] = newWidth;
235
379
 
236
- // Update the column widths state
237
380
  setColumnWidths(newColumnWidths);
238
-
239
- // Update our persisted ref to maintain widths during pagination
240
381
  persistedColumnWidthsRef.current = newColumnWidths;
241
382
 
242
- // Recalculate table width based on the new column widths
243
383
  const newTableWidth = newColumnWidths.reduce((sum, width) => sum + width, 0);
244
384
  setTableWidth(newTableWidth);
245
385
  }, [resizingIndex, startX, initialWidth, enhancedColumns, columnWidths]);
@@ -250,7 +390,6 @@ function ReactDataTable({
250
390
  setInitialWidth(null);
251
391
  }, []);
252
392
 
253
- // Add resize event listeners
254
393
  useEffect(() => {
255
394
  if (resizingIndex !== null) {
256
395
  document.addEventListener('mousemove', handleMouseMove);
@@ -299,7 +438,6 @@ function ReactDataTable({
299
438
  limit: defaultLimit
300
439
  });
301
440
 
302
- // Update the data but maintain our column widths
303
441
  setData(prev => ({
304
442
  pages: [...prev.pages, nextData],
305
443
  meta: nextData.meta
@@ -423,7 +561,7 @@ function ReactDataTable({
423
561
 
424
562
  {
425
563
  flatData.length === 0 && !loading ? (
426
- <div className="flex items-center justify-center" style={{ height }}>
564
+ <div className="flex items-center justify-center" style={{ height: dynamicHeight }}>
427
565
  <div className="text-gray-500">
428
566
  {emptyText || 'No data available'}
429
567
  </div>
@@ -433,13 +571,18 @@ function ReactDataTable({
433
571
  <div
434
572
  ref={tableContainerRef}
435
573
  className="overflow-auto w-full"
436
- style={{ maxHeight, height }}
574
+ style={{
575
+ maxHeight: dynamicHeight,
576
+ height: dynamicHeight
577
+ }}
437
578
  onScroll={(e) => handleScroll(e.currentTarget)}
438
579
  >
439
580
  <table
440
581
  className="w-full border-collapse"
441
582
  style={{
442
583
  tableLayout: 'fixed',
584
+ width: `${tableWidth}px`, // FIXED: Use calculated table width for scrolling
585
+ minWidth: '100%'
443
586
  }}
444
587
  >
445
588
  <thead
@@ -455,16 +598,14 @@ function ReactDataTable({
455
598
  return (
456
599
  <th
457
600
  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
- }`}
601
+ className={`text-left font-normal h-[40px] border-b border-t border-solid border-[#e4e3e2] relative select-none ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
602
+ } ${isReorderable ? 'cursor-move' : ''} ${isDropTarget ? 'bg-blue-400' : ''
603
+ }`}
463
604
  style={{
464
605
  width: `${width}px`,
465
606
  minWidth: `${width}px`,
466
607
  maxWidth: `${width}px`,
467
- textAlign: column.textAlign,
608
+ textAlign: column.textAlign || "left",
468
609
  }}
469
610
  draggable={isReorderable}
470
611
  onDragStart={(e) => handleDragStart(e, columnIndex)}
@@ -480,9 +621,8 @@ function ReactDataTable({
480
621
  {/* Resize handle - Only show if column is resizable */}
481
622
  {column.resizable !== false && (
482
623
  <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`}
624
+ 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'
625
+ } transition-colors duration-200`}
486
626
  onMouseDown={(e) => handleResizeStart(e, columnIndex)}
487
627
  style={{
488
628
  touchAction: 'none',
@@ -501,41 +641,128 @@ function ReactDataTable({
501
641
  {flatData.length > 0 ? (
502
642
  flatData.map((row, rowIndex) => {
503
643
  const isLastRow = rowIndex === flatData.length - 1;
644
+ const isExpanded = (expandedRows || {})[row.id];
645
+ const subRows = getSubTableRows(row);
646
+
504
647
  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}
648
+ <React.Fragment key={row.id}>
649
+ <tr
650
+ 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`}
651
+ style={{
652
+ height: `${rowHeights}px`,
653
+ ...rowStyle,
654
+ ...(typeof rowStyle === 'function' ? rowStyle(row, rowIndex) : {})
655
+ }}
656
+ onClick={() => handleRowClick(row, rowIndex, flatData)}
657
+ >
658
+ {enhancedColumns.map((column, columnIndex) => {
659
+ const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
660
+
661
+ return (
662
+ <td
663
+ key={column.accessorKey || column.id}
664
+ className={`text-left font-normal ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
665
+ } ${column?.cellProps?.className || ''}`}
666
+ style={{
667
+ width: `${width}px`,
668
+ minWidth: `${width}px`,
669
+ maxWidth: `${width}px`,
670
+ textAlign: column.textAlign || "left",
671
+ ...column?.cellProps?.style,
672
+ overflow: 'hidden',
673
+ textOverflow: 'ellipsis',
674
+ whiteSpace: 'nowrap'
675
+ }}
676
+ >
677
+ {typeof column.cell === 'function' ? column.cell({ row }) : null}
678
+ </td>
679
+ );
680
+ })}
681
+ </tr>
682
+ {/* Subtable row */}
683
+ {isExpanded && subRows.length > 0 && (
684
+ <tr className="bg-gray-50 border-b border-gray-200">
685
+ <td colSpan={enhancedColumns.length} className="p-0">
686
+ <div className="bg-white border border-gray-200" style={{ width: subTableWidth, maxWidth: '100%' }}>
687
+ <div
688
+ className="sub-table-container"
689
+ style={{
690
+ maxHeight: "300px",
691
+ width: '100%',
692
+ overflow: 'auto',
693
+ scrollbarWidth: 'thin',
694
+ scrollbarColor: '#cbd5e0 #f7fafc',
695
+ }}
696
+ >
697
+ <style>
698
+ {`
699
+ .sub-table-container::-webkit-scrollbar {
700
+ width: 6px;
701
+ height: 6px;
702
+ }
703
+ .sub-table-container::-webkit-scrollbar-track {
704
+ background: #f7fafc;
705
+ border-radius: 3px;
706
+ }
707
+ .sub-table-container::-webkit-scrollbar-thumb {
708
+ background: #cbd5e0;
709
+ border-radius: 3px;
710
+ }
711
+ .sub-table-container::-webkit-scrollbar-thumb:hover {
712
+ background: #a0aec0;
713
+ }
714
+ `}
715
+ </style>
716
+ <table className="w-full" style={{ tableLayout: 'fixed' }}>
717
+ <thead className="bg-gray-100 sticky top-0 z-10">
718
+ <tr>
719
+ {subTableColumns.map((col, colIndex) => {
720
+ return (
721
+ <th
722
+ key={col.id || col.accessorKey || colIndex}
723
+ 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' : ''
724
+ }`}
725
+ style={{
726
+ width: col.size ? `${col.size}px` : "auto",
727
+ minWidth: col.minWidth ? `${col.minWidth}px` : "80px",
728
+ }}
729
+ >
730
+ {typeof col.header === 'function' ? col.header() : (col.header || col.name)}
731
+ </th>
732
+
733
+ );
734
+ })}
735
+ </tr>
736
+ </thead>
737
+ <tbody>
738
+ {subRows.map((subRow, subRowIndex) => (
739
+ <tr key={subRow.id} className="border-b border-gray-100 hover:bg-gray-50">
740
+ {subTableColumns.map((col, colIndex) => {
741
+ return (
742
+ <td
743
+ key={col.id || col.accessorKey || colIndex}
744
+ className={`px-4 py-2 text-sm text-gray-900 ${colIndex < subTableColumns.length - 1 ? 'border-r border-gray-200' : ''
745
+ }`}
746
+ style={{
747
+ width: col.size ? `${col.size}px` : "auto",
748
+ minWidth: col.minWidth ? `${col.minWidth}px` : "80px",
749
+ }}
750
+ >
751
+ {col.cell ? col.cell({ row: subRow }) : subRow[col.accessorKey || col.id]}
752
+ </td>
753
+
754
+ );
755
+ })}
756
+ </tr>
757
+ ))}
758
+ </tbody>
759
+ </table>
760
+ </div>
761
+ </div>
535
762
  </td>
536
- );
537
- })}
538
- </tr>
763
+ </tr>
764
+ )}
765
+ </React.Fragment>
539
766
  );
540
767
  })
541
768
  ) : (