@trackunit/react-table 1.3.139 → 1.3.141
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/index.cjs.css +12 -0
- package/index.cjs.js +607 -65
- package/index.esm.css +12 -0
- package/index.esm.js +611 -69
- package/package.json +10 -10
- package/src/Table.d.ts +2 -0
- package/src/components/TextWithTooltip.d.ts +13 -0
- package/src/hooks/useCellsOffset.d.ts +21 -0
- package/src/hooks/useColumnDragDrop.d.ts +25 -0
- package/src/menus/ColumnActions.d.ts +14 -0
- package/src/menus/ColumnFilter.d.ts +2 -1
- package/src/menus/ColumnSorting.d.ts +19 -0
- package/src/translation.d.ts +2 -2
- package/src/types.d.ts +12 -0
package/index.esm.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-library-translation';
|
|
3
|
-
import { MenuItem, Icon, Button, useOverflowItems, MoreMenu, MenuList, Spacer, Tooltip, Popover, PopoverTrigger, PopoverContent, Text,
|
|
3
|
+
import { MenuItem, Icon, Button, useOverflowItems, MoreMenu, MenuList, Spacer, Tooltip, Popover, PopoverTrigger, PopoverContent, cvaInteractableItem, Text, IconButton, Card, Spinner, EmptyState } from '@trackunit/react-components';
|
|
4
4
|
import { objectValues, nonNullable, objectKeys, objectEntries } from '@trackunit/shared-utils';
|
|
5
|
-
import { useMemo, Children, cloneElement, useRef, useEffect, useCallback,
|
|
5
|
+
import { useMemo, Children, cloneElement, useRef, useState, useEffect, useCallback, createElement } from 'react';
|
|
6
6
|
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
7
7
|
import { Link } from '@tanstack/react-router';
|
|
8
|
-
import { cvaLabel, ToggleSwitch, RadioGroup, RadioItem, Checkbox } from '@trackunit/react-form-components';
|
|
8
|
+
import { Search, cvaLabel, ToggleSwitch, RadioGroup, RadioItem, Checkbox } from '@trackunit/react-form-components';
|
|
9
9
|
import update from 'immutability-helper';
|
|
10
10
|
import { DndProvider, useDrop, useDrag } from 'react-dnd';
|
|
11
11
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
12
12
|
import { flexRender, createColumnHelper, useReactTable, getSortedRowModel, getCoreRowModel } from '@tanstack/react-table';
|
|
13
13
|
export { createColumnHelper } from '@tanstack/react-table';
|
|
14
|
-
import { TableRoot, Thead, Tr, Th,
|
|
14
|
+
import { TableRoot, Thead, Tr, Th, ResizeHandle, Tbody, Td } from '@trackunit/react-table-base-components';
|
|
15
15
|
import { useInfiniteScroll, noPagination } from '@trackunit/react-table-pagination';
|
|
16
16
|
import { twMerge } from 'tailwind-merge';
|
|
17
17
|
import { SortOrder, validateStringAsAssetSortByProperty } from '@trackunit/react-core-contexts-api';
|
|
@@ -19,6 +19,12 @@ import { SortOrder, validateStringAsAssetSortByProperty } from '@trackunit/react
|
|
|
19
19
|
var defaultTranslations = {
|
|
20
20
|
"layout.actions.reset": "Reset",
|
|
21
21
|
"table.actionsheet.selected": "{{count}} selected",
|
|
22
|
+
"table.columnActions.clearSorting": "Clear sorting",
|
|
23
|
+
"table.columnActions.hideColumn": "Hide column",
|
|
24
|
+
"table.columnActions.pinColumn": "Pin column",
|
|
25
|
+
"table.columnActions.sortAscending": "Sort ascending",
|
|
26
|
+
"table.columnActions.sortDescending": "Sort descending",
|
|
27
|
+
"table.columnActions.unPinColumn": "Unpin column",
|
|
22
28
|
"table.columnFilters.columns": "Columns",
|
|
23
29
|
"table.columnFilters.hiddenColumnCount": "{{count}} column hidden",
|
|
24
30
|
"table.columnFilters.hiddenColumnsCount": "{{count}} columns hidden",
|
|
@@ -289,8 +295,8 @@ const cvaColumnFilterGrabbable = cvaMerge(["flex", "items-center", "justify-cent
|
|
|
289
295
|
*/
|
|
290
296
|
const ColumnFilter = ({ columns, setColumnOrder, defaultColumnOrder = [], columnOrder, onUserEvent, }) => {
|
|
291
297
|
const [t] = useTranslation();
|
|
292
|
-
const numberOfHiddenColumns = columns.filter(column => !column.getIsVisible()).length;
|
|
293
298
|
const initialSetupRef = useRef(false);
|
|
299
|
+
const [searchText, setSearchText] = useState("");
|
|
294
300
|
useEffect(() => {
|
|
295
301
|
if (!initialSetupRef.current && defaultColumnOrder.length > 0) {
|
|
296
302
|
setColumnOrder(defaultColumnOrder);
|
|
@@ -300,37 +306,35 @@ const ColumnFilter = ({ columns, setColumnOrder, defaultColumnOrder = [], column
|
|
|
300
306
|
const resetHiddenColumns = useCallback(() => {
|
|
301
307
|
columns.forEach(column => column.toggleVisibility(!column.columnDef.meta?.hiddenByDefault));
|
|
302
308
|
setColumnOrder(defaultColumnOrder);
|
|
309
|
+
columns.forEach(column => column.pin(column.columnDef.meta?.pinned ?? false));
|
|
303
310
|
}, [columns, defaultColumnOrder, setColumnOrder]);
|
|
304
311
|
const sortedColumns = useMemo(() => {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return t("table.columnFilters.hiddenColumnsCount", { count: numberOfHiddenColumns });
|
|
312
|
+
let filteredColumns = columns;
|
|
313
|
+
if (searchText.trim() !== "") {
|
|
314
|
+
const trimmedSearchText = searchText.trim().toLowerCase();
|
|
315
|
+
filteredColumns = columns.filter(column => {
|
|
316
|
+
const label = column.columnDef.meta?.columnFilterLabel ?? column.columnDef.header?.toString() ?? "";
|
|
317
|
+
return label.toLowerCase().includes(trimmedSearchText);
|
|
318
|
+
});
|
|
313
319
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
+
return columnOrder && columnOrder.length > 0
|
|
321
|
+
? filteredColumns.sort((a, b) => columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id))
|
|
322
|
+
: filteredColumns;
|
|
323
|
+
}, [columnOrder, columns, searchText]);
|
|
324
|
+
const visibleColumnsCount = useMemo(() => {
|
|
325
|
+
return columns.filter(col => col.getIsVisible()).length;
|
|
326
|
+
}, [columns]);
|
|
327
|
+
return (jsx(DndProvider, { backend: HTML5Backend, children: jsx(Tooltip, { label: t("table.columnFilters.tooltip"), placement: "bottom", children: jsxs(Popover, { placement: "bottom-start", children: [jsx(PopoverTrigger, { children: jsx(Button, { prefix: jsx(Icon, { name: "ViewColumns", size: "small" }), size: "small", variant: "secondary", children: jsx("span", { className: "hidden md:block", children: t("table.columnFilters.columns") }) }) }), jsx(PopoverContent, { children: () => (jsxs(MenuList, { className: "relative max-h-[55vh] w-72 overflow-y-auto", children: [jsxs("div", { className: "mb-2 flex items-center gap-1", children: [jsx(Search, { autoFocus: true, className: "flex-1", fieldSize: "small", id: "column-search", onChange: e => setSearchText(e.currentTarget.value.toLowerCase()), onClear: () => setSearchText(""), placeholder: "Search", value: searchText }), jsx(Button, { onClick: resetHiddenColumns, size: "small", variant: "ghost-neutral", children: t("layout.actions.reset") })] }), jsx(ColumnFiltersDragAndDrop, { columns: sortedColumns, isSortDisabled: !!searchText, onUserEvent: onUserEvent, setColumnOrder: setColumnOrder, visibleColumnsCount: visibleColumnsCount })] })) })] }) }) }));
|
|
320
328
|
};
|
|
321
|
-
const ColumnFiltersDragAndDrop = ({ columns, setColumnOrder, onUserEvent,
|
|
329
|
+
const ColumnFiltersDragAndDrop = ({ columns, setColumnOrder, onUserEvent, isSortDisabled, visibleColumnsCount, }) => {
|
|
322
330
|
const [localColumns, setLocalColumns] = useState(columns);
|
|
323
331
|
useEffect(() => {
|
|
324
332
|
setLocalColumns(columns);
|
|
325
333
|
}, [columns]);
|
|
326
334
|
const onColumDrop = useCallback(() => {
|
|
327
|
-
const
|
|
328
|
-
const firstLocalColumnId = localColumns[0]?.id;
|
|
329
|
-
const newColumnOrder = firstElement === firstLocalColumnId
|
|
330
|
-
? localColumns.map(column => column.id)
|
|
331
|
-
: [firstElement, ...localColumns.map(column => column.id)].filter((id) => id !== undefined);
|
|
335
|
+
const newColumnOrder = localColumns.map(column => column.id);
|
|
332
336
|
setColumnOrder(newColumnOrder);
|
|
333
|
-
}, [localColumns, setColumnOrder
|
|
337
|
+
}, [localColumns, setColumnOrder]);
|
|
334
338
|
const onCancelColumDrop = useCallback(() => {
|
|
335
339
|
setLocalColumns(columns);
|
|
336
340
|
}, [columns]);
|
|
@@ -354,19 +358,20 @@ const ColumnFiltersDragAndDrop = ({ columns, setColumnOrder, onUserEvent, defaul
|
|
|
354
358
|
(!columnDef.header || columnDef.header.length === 0 || typeof columnDef.header === "function")) {
|
|
355
359
|
return null;
|
|
356
360
|
}
|
|
357
|
-
|
|
361
|
+
const isVisible = column.getIsVisible();
|
|
362
|
+
return (jsx(ColumnFilterItem, { disabled: !column.getCanHide() || (isVisible && visibleColumnsCount === 1), id: column.id, index: index, isSortDisabled: isSortDisabled, moveColumn: moveColumn, name: columnDef.meta?.columnFilterLabel ?? columnDef.header?.toString() ?? "", onCancelDrop: onCancelColumDrop, onDrop: onColumDrop, onToggle: (toggled, event) => {
|
|
358
363
|
column.getToggleVisibilityHandler()(event);
|
|
359
364
|
onUserEvent?.("Column Filter", {
|
|
360
365
|
columnName: column.id,
|
|
361
366
|
hidden: !toggled,
|
|
362
367
|
});
|
|
363
|
-
}, toggled:
|
|
368
|
+
}, toggled: isVisible }, column.id));
|
|
364
369
|
}) }));
|
|
365
370
|
};
|
|
366
371
|
const ItemTypes = {
|
|
367
372
|
COLUMN: "column",
|
|
368
373
|
};
|
|
369
|
-
const ColumnFilterItem = ({ name, onToggle, toggled, disabled, index, moveColumn, onDrop, onCancelDrop, id, }) => {
|
|
374
|
+
const ColumnFilterItem = ({ name, onToggle, toggled, disabled, index, moveColumn, onDrop, onCancelDrop, id, isSortDisabled, }) => {
|
|
370
375
|
const ref = useRef(null);
|
|
371
376
|
const [, drop] = useDrop({
|
|
372
377
|
accept: ItemTypes.COLUMN,
|
|
@@ -399,7 +404,7 @@ const ColumnFilterItem = ({ name, onToggle, toggled, disabled, index, moveColumn
|
|
|
399
404
|
const [{ isDragging }, drag] = useDrag({
|
|
400
405
|
type: ItemTypes.COLUMN,
|
|
401
406
|
item: { type: ItemTypes.COLUMN, id, index },
|
|
402
|
-
canDrag:
|
|
407
|
+
canDrag: !isSortDisabled,
|
|
403
408
|
collect: (monitor) => ({
|
|
404
409
|
isDragging: monitor.isDragging(),
|
|
405
410
|
}),
|
|
@@ -414,7 +419,7 @@ const ColumnFilterItem = ({ name, onToggle, toggled, disabled, index, moveColumn
|
|
|
414
419
|
return (jsxs("div", { className: cvaInteractableItem({
|
|
415
420
|
selected: false,
|
|
416
421
|
className: "grid w-full grid-cols-[min-content,1fr,min-content] items-center gap-2 py-1",
|
|
417
|
-
}), ref: ref, style: { opacity: opacity }, children: [jsx("div", { className: cvaColumnFilterGrabbable({ disabled:
|
|
422
|
+
}), ref: ref, style: { opacity: opacity }, children: [jsx("div", { className: cvaColumnFilterGrabbable({ disabled: isSortDisabled }), children: jsx(Icon, { color: "secondary", name: "EllipsisDrag", size: "small" }) }), jsxs("label", { className: "grid w-full grid-cols-[1fr,min-content] items-center gap-2", htmlFor: id, children: [jsxs("div", { className: "grid w-full grid-cols-[1fr,min-content] items-center", children: [jsx(Text, { className: cvaLabel(), children: name }), disabled ? jsx(Icon, { name: "LockClosed", size: "small" }) : null] }), jsx(ToggleSwitch, { disabled: disabled, id: id, onChange: (isToggled, event) => event && onToggle(isToggled, event), showInputFocus: false, size: "small", toggled: toggled })] })] }));
|
|
418
423
|
};
|
|
419
424
|
|
|
420
425
|
/**
|
|
@@ -431,7 +436,7 @@ const RowSpacing = ({ density, setRowDensity, onUserEvent }) => {
|
|
|
431
436
|
densityChosen: selectedDensity,
|
|
432
437
|
});
|
|
433
438
|
};
|
|
434
|
-
return (jsx(Tooltip, { dataTestId: "row-spacing", label: t("table.spacing.toolip"), placement: "bottom", children: jsxs(Popover, { placement: "bottom-start", children: [jsx(PopoverTrigger, { children: jsx(Button, { prefix: jsx(Icon, { name: "ArrowsPointingOut", size: "small" }), size: "small", variant: "
|
|
439
|
+
return (jsx(Tooltip, { dataTestId: "row-spacing", label: t("table.spacing.toolip"), placement: "bottom", children: jsxs(Popover, { placement: "bottom-start", children: [jsx(PopoverTrigger, { children: jsx(Button, { prefix: jsx(Icon, { name: "ArrowsPointingOut", size: "small" }), size: "small", variant: "secondary", children: jsx("span", { className: "hidden md:block", children: t("table.spacing") }) }) }), jsx(PopoverContent, { children: jsx(MenuList, { children: jsxs("div", { className: "flex flex-col justify-start", children: [jsx(DensitySelection, { icon: jsx(CompactIcon, {}), isSelected: density === "compact", onClick: () => handleClick("compact"), text: t("table.rowDensity.compact") }), jsx(DensitySelection, { icon: jsx(SpaciousIcon, {}), isSelected: density === "spacious", onClick: () => handleClick("spacious"), text: t("table.rowDensity.spacious") })] }) }) })] }) }));
|
|
435
440
|
};
|
|
436
441
|
const DensitySelection = ({ text, onClick, icon, isSelected }) => {
|
|
437
442
|
return (jsxs("div", { className: "flex w-full cursor-pointer items-center p-1 hover:bg-neutral-50", onClick: onClick, children: [jsx("div", { className: "mr-1 flex", children: icon }), jsx(Text, { weight: "thick", children: text }), isSelected ? (jsx("div", { className: "justify-endgrow flex", children: jsx(Icon, { name: "Check", size: "small" }) })) : null] }));
|
|
@@ -472,6 +477,363 @@ const Sorting = ({ columns, }) => {
|
|
|
472
477
|
return (jsx(Tooltip, { label: t("table.sorting.toolip"), placement: "bottom", children: jsxs(Popover, { placement: "bottom-start", children: [jsx(PopoverTrigger, { children: jsx(Button, { prefix: currentSortDirection === "asc" ? (jsx(Icon, { name: "BarsArrowUp", size: "small" })) : (jsx(Icon, { name: "BarsArrowDown", size: "small" })), size: "small", variant: "ghost-neutral", children: jsx("span", { className: "hidden sm:block", children: t("table.sorting.label") }) }) }), jsx(PopoverContent, { children: jsxs(MenuList, { className: "max-h-[55vh] overflow-y-auto", children: [jsx(RadioGroup, { id: "sortProperty", onChange: handleSelectionChange, value: currentSortValue ?? "", children: sortableColumns.map(column => (jsx(RadioItem, { className: "w-full", label: column.columnDef.header?.toString() ?? "", value: column.id }, column.id))) }), jsxs(RadioGroup, { id: "sortOrder", onChange: onSelectOrder, value: currentSortDirection, children: [jsx(RadioItem, { className: "w-full", label: t("table.sorting.ascending"), value: "asc" }), jsx(RadioItem, { className: "w-full", label: t("table.sorting.descending"), value: "desc" })] })] }) })] }) }));
|
|
473
478
|
};
|
|
474
479
|
|
|
480
|
+
/**
|
|
481
|
+
* This component is used to display a text with a tooltip.
|
|
482
|
+
* The tooltip is displayed if the text is truncated with ellipsis.
|
|
483
|
+
* The text is the text to display.
|
|
484
|
+
* The tooltipLabel is the label to display in the tooltip.
|
|
485
|
+
*/
|
|
486
|
+
const TextWithTooltip = ({ children, tooltipLabel, weight, }) => {
|
|
487
|
+
const [isTextTruncated, setIsTextTruncated] = useState(false);
|
|
488
|
+
const textRef = useRef(null);
|
|
489
|
+
const resizeObserverRef = useRef(null);
|
|
490
|
+
const debounceTimerRef = useRef(null);
|
|
491
|
+
const [hideTooltip, setHideTooltip] = useState(false);
|
|
492
|
+
const checkIfTruncated = useCallback(() => {
|
|
493
|
+
if (textRef.current) {
|
|
494
|
+
const isTruncated = textRef.current.scrollWidth > textRef.current.clientWidth;
|
|
495
|
+
setIsTextTruncated(isTruncated);
|
|
496
|
+
}
|
|
497
|
+
}, []);
|
|
498
|
+
const debouncedCheckTruncation = useCallback(() => {
|
|
499
|
+
if (debounceTimerRef.current !== null) {
|
|
500
|
+
window.clearTimeout(debounceTimerRef.current);
|
|
501
|
+
}
|
|
502
|
+
debounceTimerRef.current = window.setTimeout(() => {
|
|
503
|
+
checkIfTruncated();
|
|
504
|
+
debounceTimerRef.current = null;
|
|
505
|
+
}, 150);
|
|
506
|
+
}, [checkIfTruncated]);
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
const timeoutId = setTimeout(checkIfTruncated, 0);
|
|
509
|
+
if (resizeObserverRef.current) {
|
|
510
|
+
resizeObserverRef.current.disconnect();
|
|
511
|
+
}
|
|
512
|
+
resizeObserverRef.current = new ResizeObserver(() => {
|
|
513
|
+
debouncedCheckTruncation();
|
|
514
|
+
});
|
|
515
|
+
if (textRef.current) {
|
|
516
|
+
resizeObserverRef.current.observe(textRef.current);
|
|
517
|
+
}
|
|
518
|
+
window.addEventListener("resize", debouncedCheckTruncation);
|
|
519
|
+
return () => {
|
|
520
|
+
clearTimeout(timeoutId);
|
|
521
|
+
if (debounceTimerRef.current !== null) {
|
|
522
|
+
window.clearTimeout(debounceTimerRef.current);
|
|
523
|
+
}
|
|
524
|
+
if (resizeObserverRef.current) {
|
|
525
|
+
resizeObserverRef.current.disconnect();
|
|
526
|
+
resizeObserverRef.current = null;
|
|
527
|
+
}
|
|
528
|
+
window.removeEventListener("resize", debouncedCheckTruncation);
|
|
529
|
+
};
|
|
530
|
+
}, [debouncedCheckTruncation, checkIfTruncated, children]);
|
|
531
|
+
return tooltipLabel ? (jsx(Tooltip, { disabled: !isTextTruncated || hideTooltip, label: tooltipLabel, placement: "bottom", children: jsx(Text, { className: "truncate", ref: textRef, type: "span", weight: weight, children: children }) })) : (jsx("div", { onMouseDown: () => setHideTooltip(true), onMouseUp: () => setHideTooltip(false), children: jsx(Text, { className: "truncate", ref: textRef, type: "span", weight: weight, children: children }) }));
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* The `useCellsOffset` hook calculates column offsets for pinned columns ("left" or "right") in a table.
|
|
536
|
+
*
|
|
537
|
+
* @template TData The type of the row data in the table.
|
|
538
|
+
* @param {Header<TData, unknown>[][]} headerGroups - Array of table header groups.
|
|
539
|
+
* @param {string | null} draggingColumnId - The ID of the column currently being dragged. If null, no dragging is occurring.
|
|
540
|
+
* @returns {Array<object>} Returns an array of objects containing `leftOffsets` and `rightOffsets` for each header group.
|
|
541
|
+
*/
|
|
542
|
+
const useCellsOffset = (headerGroups, draggingColumnId) => {
|
|
543
|
+
return useMemo(() => {
|
|
544
|
+
return headerGroups.map(headers => {
|
|
545
|
+
const leftOffsets = {};
|
|
546
|
+
const rightOffsets = {};
|
|
547
|
+
let left = 0;
|
|
548
|
+
let right = 0;
|
|
549
|
+
const rightPinnedCells = headers.filter(cell => cell.column.getIsPinned() === "right");
|
|
550
|
+
for (let i = rightPinnedCells.length - 1; i >= 0; i--) {
|
|
551
|
+
const header = rightPinnedCells[i];
|
|
552
|
+
if (header?.column && header.column.id !== draggingColumnId) {
|
|
553
|
+
rightOffsets[header.column.id] = { id: header.column.id, offset: right, pinned: "right" };
|
|
554
|
+
right += header.column.getSize();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
headers.forEach(header => {
|
|
558
|
+
if (header.column.id === draggingColumnId) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (header.column.getIsPinned() === "left") {
|
|
562
|
+
leftOffsets[header.column.id] = { id: header.column.id, offset: left, pinned: "left" };
|
|
563
|
+
left += header.column.getSize();
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
return {
|
|
567
|
+
leftOffsets,
|
|
568
|
+
rightOffsets,
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
}, [headerGroups, draggingColumnId]);
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Hook for handling column drag and drop functionality in a table
|
|
576
|
+
*
|
|
577
|
+
* @param options - Options including the ReactTable instance and optional callbacks
|
|
578
|
+
* @returns Object containing drag state and handler functions
|
|
579
|
+
*/
|
|
580
|
+
const useColumnDragDrop = ({ table, }) => {
|
|
581
|
+
const [draggingColumnId, setDraggingColumnId] = useState(null);
|
|
582
|
+
const [dropTargetColumnId, setDropTargetColumnId] = useState(null);
|
|
583
|
+
const [dropPosition, setDropPosition] = useState(null);
|
|
584
|
+
const [droppedColumnId, setDroppedColumnId] = useState(null);
|
|
585
|
+
const startDragging = useCallback((columnId) => {
|
|
586
|
+
setDraggingColumnId(columnId);
|
|
587
|
+
}, []);
|
|
588
|
+
const endDragging = useCallback(() => {
|
|
589
|
+
setDraggingColumnId(null);
|
|
590
|
+
setDropTargetColumnId(null);
|
|
591
|
+
setDropPosition(null);
|
|
592
|
+
}, []);
|
|
593
|
+
const setDropTarget = useCallback((columnId, position) => {
|
|
594
|
+
setDropTargetColumnId(columnId);
|
|
595
|
+
setDropPosition(position);
|
|
596
|
+
}, []);
|
|
597
|
+
// Helper function to generate the right positioning for a column move
|
|
598
|
+
const moveColumn = useCallback((columnOrder, fromId, toId, position) => {
|
|
599
|
+
const newColumnOrder = [...columnOrder];
|
|
600
|
+
const fromIndex = newColumnOrder.indexOf(fromId);
|
|
601
|
+
if (fromIndex !== -1) {
|
|
602
|
+
// Remove the moving column
|
|
603
|
+
newColumnOrder.splice(fromIndex, 1);
|
|
604
|
+
// Calculate the new position
|
|
605
|
+
let newIndex = newColumnOrder.indexOf(toId);
|
|
606
|
+
if (position === "right") {
|
|
607
|
+
newIndex += 1;
|
|
608
|
+
}
|
|
609
|
+
// Insert at the new position
|
|
610
|
+
newColumnOrder.splice(newIndex, 0, fromId);
|
|
611
|
+
}
|
|
612
|
+
return newColumnOrder;
|
|
613
|
+
}, []);
|
|
614
|
+
const handleDrop = useCallback(() => {
|
|
615
|
+
if (draggingColumnId && dropTargetColumnId && dropPosition) {
|
|
616
|
+
// Find the actual column objects
|
|
617
|
+
const columns = table.getAllColumns();
|
|
618
|
+
const fromColumn = columns.find(col => col.id === draggingColumnId);
|
|
619
|
+
const toColumn = columns.find(col => col.id === dropTargetColumnId);
|
|
620
|
+
if (fromColumn && toColumn) {
|
|
621
|
+
// Calculate new column order
|
|
622
|
+
const newColumnOrder = moveColumn(table.getState().columnOrder, draggingColumnId, dropTargetColumnId, dropPosition);
|
|
623
|
+
// Move the column in the table's column order
|
|
624
|
+
table.setColumnOrder(newColumnOrder);
|
|
625
|
+
setDroppedColumnId(draggingColumnId);
|
|
626
|
+
setTimeout(() => {
|
|
627
|
+
setDroppedColumnId(null);
|
|
628
|
+
}, 800);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
endDragging();
|
|
632
|
+
}, [draggingColumnId, dropTargetColumnId, dropPosition, endDragging, moveColumn, table]);
|
|
633
|
+
// Generate header props to apply drag and drop functionality
|
|
634
|
+
const getHeaderProps = useCallback((headerId, styles) => {
|
|
635
|
+
const header = table
|
|
636
|
+
.getHeaderGroups()
|
|
637
|
+
.flatMap(group => group.headers)
|
|
638
|
+
.find(h => h.id === headerId);
|
|
639
|
+
return {
|
|
640
|
+
className: "", // Don't add dragging-column class here, we'll add it in the Table component
|
|
641
|
+
"data-column-id": headerId, // Add this attribute for identification
|
|
642
|
+
style: draggingColumnId !== headerId && header
|
|
643
|
+
? {
|
|
644
|
+
width: header.getSize(),
|
|
645
|
+
minWidth: header.column.columnDef.minSize,
|
|
646
|
+
maxWidth: header.column.columnDef.maxSize,
|
|
647
|
+
textAlign: header.column.columnDef.meta?.alignment || "left",
|
|
648
|
+
flexGrow: header.column.columnDef.meta?.shouldGrow ? 1 : 0,
|
|
649
|
+
...styles,
|
|
650
|
+
}
|
|
651
|
+
: undefined,
|
|
652
|
+
onDragLeave: () => {
|
|
653
|
+
if (dropTargetColumnId === headerId) {
|
|
654
|
+
setDropTarget(null, null);
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
onDragOver: (e) => {
|
|
658
|
+
e.preventDefault();
|
|
659
|
+
if (draggingColumnId && draggingColumnId !== headerId) {
|
|
660
|
+
// Determine drop position based on mouse position
|
|
661
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
662
|
+
const mouseX = e.clientX;
|
|
663
|
+
const threshold = rect.left + rect.width / 2;
|
|
664
|
+
const position = mouseX < threshold ? "left" : "right";
|
|
665
|
+
setDropTarget(headerId, position);
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
onDrop: (e) => {
|
|
669
|
+
e.preventDefault();
|
|
670
|
+
handleDrop();
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
}, [draggingColumnId, dropTargetColumnId, handleDrop, setDropTarget, table]);
|
|
674
|
+
// Generate drag handle props
|
|
675
|
+
const getDragHandleProps = useCallback((headerId, isPlaceholder, pinned) => ({
|
|
676
|
+
className: "invisible mr-1 cursor-grab text-gray-400 group-hover:visible",
|
|
677
|
+
draggable: !pinned && !isPlaceholder,
|
|
678
|
+
onDragEnd: (e) => {
|
|
679
|
+
endDragging();
|
|
680
|
+
e.preventDefault();
|
|
681
|
+
e.stopPropagation();
|
|
682
|
+
},
|
|
683
|
+
onDragStart: (e) => {
|
|
684
|
+
e.dataTransfer.setData("columnId", headerId);
|
|
685
|
+
e.dataTransfer.effectAllowed = "move";
|
|
686
|
+
// Create custom drag ghost element
|
|
687
|
+
const column = table.getColumn(headerId);
|
|
688
|
+
if (column) {
|
|
689
|
+
// Get column header text
|
|
690
|
+
const headerText = typeof column.columnDef.header === "string" ? column.columnDef.header : column.id;
|
|
691
|
+
// Create custom drag ghost element
|
|
692
|
+
const dragGhost = document.createElement("div");
|
|
693
|
+
dragGhost.className =
|
|
694
|
+
"bg-white shadow-md rounded p-3 flex items-center opacity-50 gap-1 border border-gray-300";
|
|
695
|
+
dragGhost.style.position = "absolute";
|
|
696
|
+
dragGhost.style.top = "-1000px"; // Position off-screen initially
|
|
697
|
+
dragGhost.style.zIndex = "9999";
|
|
698
|
+
dragGhost.style.pointerEvents = "none";
|
|
699
|
+
// Add minimum width to prevent narrow ghost for short column names
|
|
700
|
+
dragGhost.style.minWidth = "120px";
|
|
701
|
+
// Add drag icon with arrows in all directions
|
|
702
|
+
const iconElement = document.createElement("span");
|
|
703
|
+
iconElement.innerHTML = `
|
|
704
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
705
|
+
<path d="M5 3L2 6L5 9" fill="none"/>
|
|
706
|
+
<path d="M11 3L14 6L11 9" fill="none"/>
|
|
707
|
+
<path d="M2 6H14" fill="none"/>
|
|
708
|
+
</svg>
|
|
709
|
+
|
|
710
|
+
`;
|
|
711
|
+
iconElement.className = "text-gray-500 flex-shrink-0 pl-4 pt-1";
|
|
712
|
+
iconElement.style.marginRight = "6px";
|
|
713
|
+
// Add column title
|
|
714
|
+
const titleElement = document.createElement("span");
|
|
715
|
+
titleElement.textContent = headerText;
|
|
716
|
+
titleElement.className = "font-medium text-sm";
|
|
717
|
+
// Append elements to the drag ghost
|
|
718
|
+
dragGhost.appendChild(iconElement);
|
|
719
|
+
dragGhost.appendChild(titleElement);
|
|
720
|
+
// Append to body temporarily (will be removed after drag)
|
|
721
|
+
document.body.appendChild(dragGhost);
|
|
722
|
+
// Set custom drag image
|
|
723
|
+
e.dataTransfer.setDragImage(dragGhost, 20, 20);
|
|
724
|
+
// Clean up the ghost element after drag operation completes
|
|
725
|
+
setTimeout(() => {
|
|
726
|
+
document.body.removeChild(dragGhost);
|
|
727
|
+
}, 0);
|
|
728
|
+
}
|
|
729
|
+
startDragging(headerId);
|
|
730
|
+
e.stopPropagation();
|
|
731
|
+
},
|
|
732
|
+
}), [endDragging, startDragging, table]);
|
|
733
|
+
// Render the drop indicator
|
|
734
|
+
const renderDropIndicator = useCallback((columnId) => {
|
|
735
|
+
if (dropTargetColumnId === columnId && dropPosition) {
|
|
736
|
+
return (jsx("div", { className: "bg-primary-300 absolute bottom-[-2px] top-[-2px] w-[3px]", style: {
|
|
737
|
+
height: "calc(100% + 4px)",
|
|
738
|
+
overflow: "visible",
|
|
739
|
+
// Position to overlay the parent's border
|
|
740
|
+
left: dropPosition === "left" ? "-2px" : "auto",
|
|
741
|
+
right: dropPosition === "right" ? "-2px" : "auto",
|
|
742
|
+
pointerEvents: "none",
|
|
743
|
+
zIndex: 100000,
|
|
744
|
+
// Add a subtle shadow to help it stand out
|
|
745
|
+
boxShadow: "0 0 2px rgba(0, 0, 0, 0.2)",
|
|
746
|
+
} }));
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}, [dropTargetColumnId, dropPosition]);
|
|
750
|
+
// Check if a cell is a drop target
|
|
751
|
+
const isCellDropTarget = useCallback((columnId) => dropTargetColumnId === columnId, [dropTargetColumnId]);
|
|
752
|
+
// Generate cell props to collapse dragged columns
|
|
753
|
+
const getCellProps = useCallback((columnId, styles) => {
|
|
754
|
+
// Find a representative cell for this column to get sizing info
|
|
755
|
+
const column = table.getColumn(columnId);
|
|
756
|
+
return {
|
|
757
|
+
className: draggingColumnId === columnId ? "dragging-column" : "",
|
|
758
|
+
style: draggingColumnId !== columnId && column
|
|
759
|
+
? {
|
|
760
|
+
width: column.getSize(),
|
|
761
|
+
minWidth: column.columnDef.minSize,
|
|
762
|
+
maxWidth: column.columnDef.maxSize,
|
|
763
|
+
textAlign: column.columnDef.meta?.alignment || "left",
|
|
764
|
+
flexGrow: column.columnDef.meta?.shouldGrow ? 1 : 0,
|
|
765
|
+
...styles,
|
|
766
|
+
}
|
|
767
|
+
: undefined,
|
|
768
|
+
};
|
|
769
|
+
}, [draggingColumnId, table]);
|
|
770
|
+
return useMemo(() => ({
|
|
771
|
+
draggingColumnId,
|
|
772
|
+
dropTargetColumnId,
|
|
773
|
+
dropPosition,
|
|
774
|
+
droppedColumnId,
|
|
775
|
+
getHeaderProps,
|
|
776
|
+
getDragHandleProps,
|
|
777
|
+
renderDropIndicator,
|
|
778
|
+
isCellDropTarget,
|
|
779
|
+
getCellProps,
|
|
780
|
+
}), [
|
|
781
|
+
draggingColumnId,
|
|
782
|
+
dropTargetColumnId,
|
|
783
|
+
dropPosition,
|
|
784
|
+
droppedColumnId,
|
|
785
|
+
getHeaderProps,
|
|
786
|
+
getDragHandleProps,
|
|
787
|
+
renderDropIndicator,
|
|
788
|
+
isCellDropTarget,
|
|
789
|
+
getCellProps,
|
|
790
|
+
]);
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* `ColumnActions` component to render column action controls for a table.
|
|
795
|
+
*
|
|
796
|
+
* @template TData The type of the table row data.
|
|
797
|
+
* @param {object} props Component properties.
|
|
798
|
+
* @param {Header<TData, unknown>} props.header The header of the current column provided by React Table.
|
|
799
|
+
* @param {number} props.visibleColumnsCount The number of visible columns in the table.
|
|
800
|
+
*/
|
|
801
|
+
const ColumnActions = ({ header, visibleColumnsCount, }) => {
|
|
802
|
+
const [t] = useTranslation();
|
|
803
|
+
const isSorted = header.column.getIsSorted();
|
|
804
|
+
const canSort = header.column.getCanSort();
|
|
805
|
+
const canHide = header.column.getCanHide();
|
|
806
|
+
const canPin = header.column.getCanPin();
|
|
807
|
+
const isPinned = header.column.getIsPinned();
|
|
808
|
+
const isAnyActionAvailable = canSort || canPin || (canHide && visibleColumnsCount > 1);
|
|
809
|
+
if (!isAnyActionAvailable) {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
return (jsx(MoreMenu, { customButton: jsx(IconButton, { icon: jsx(Icon, { name: "EllipsisVertical", size: "small" }), onClick: event => event.stopPropagation(), size: "extraSmall", variant: "ghost-neutral" }), children: close => (jsxs(MenuList, { children: [canSort && (isSorted === "desc" || !isSorted) ? (jsx(MenuItem, { label: t("table.columnActions.sortAscending"), onClick: () => header.column.toggleSorting(false), prefix: jsx(Icon, { name: "ArrowUp", size: "medium" }) })) : null, canSort && (isSorted === "asc" || !isSorted) ? (jsx(MenuItem, { label: t("table.columnActions.sortDescending"), onClick: () => header.column.toggleSorting(true), prefix: jsx(Icon, { name: "ArrowDown", size: "medium" }) })) : null, canSort && isSorted ? (jsx(MenuItem, { label: t("table.columnActions.clearSorting"), onClick: () => header.column.clearSorting(), prefix: jsx(Icon, { name: "ArrowsUpDown", size: "medium" }) })) : null, canHide && visibleColumnsCount > 1 ? (jsx(MenuItem, { label: t("table.columnActions.hideColumn"), onClick: () => header.column.toggleVisibility(false), prefix: jsx(Icon, { name: "EyeSlash", size: "medium" }) })) : null, canPin ? (jsx(MenuItem, { label: t(isPinned ? "table.columnActions.unPinColumn" : "table.columnActions.pinColumn"), onClick: () => {
|
|
813
|
+
header.column.pin(isPinned ? false : "left");
|
|
814
|
+
close();
|
|
815
|
+
}, prefix: jsx(Icon, { name: isPinned ? "Unpin" : "Pin", size: "medium" }) })) : null] })) }));
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The ColumnSorting is used in the table header to indicate the current sort order of the column.
|
|
820
|
+
* This is a visual only component and does not handle the sorting logic.
|
|
821
|
+
*
|
|
822
|
+
* In most cases, we recommend using the Table Component which already handles sorting indication.
|
|
823
|
+
* However, if you need to construct a custom table, the Table Base Components can be utilized.
|
|
824
|
+
*
|
|
825
|
+
* @param {object} props - Props for the sorting indicator.
|
|
826
|
+
* @param {boolean} [props.canSort] - Indicates the header sorting ability state (ascending/descending).
|
|
827
|
+
* @param {boolean} [props.sortingState=false] - Indicates the sorting state (ascending/descending).
|
|
828
|
+
* @returns {ReactElement | null} A React element representing the sorting indicator.
|
|
829
|
+
*/
|
|
830
|
+
const ColumnSorting = ({ canSort, sortingState = false }) => {
|
|
831
|
+
if (!canSort || !sortingState) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
return (jsx("div", { className: "flex items-center", children: jsx(Icon, { className: "text-blue-600", name: sortingState === "asc" ? "ArrowUp" : "ArrowDown", size: "small" }) }));
|
|
835
|
+
};
|
|
836
|
+
|
|
475
837
|
/**
|
|
476
838
|
* Table component for displaying large sets of data in tables with infinite scroll, sorting, filtering and others.
|
|
477
839
|
*
|
|
@@ -481,7 +843,7 @@ const Sorting = ({ columns, }) => {
|
|
|
481
843
|
/**
|
|
482
844
|
*
|
|
483
845
|
*/
|
|
484
|
-
const Table = ({ rowHeight =
|
|
846
|
+
const Table = ({ rowHeight = 50, ...props }) => {
|
|
485
847
|
//we need a reference to the scrolling element for logic down below
|
|
486
848
|
const tableContainerRef = useRef(null);
|
|
487
849
|
const [t] = useTranslation();
|
|
@@ -491,23 +853,81 @@ const Table = ({ rowHeight = 75, ...props }) => {
|
|
|
491
853
|
rowSize: props.getRowModel().rows.length || 0,
|
|
492
854
|
rowHeight,
|
|
493
855
|
});
|
|
856
|
+
const visibleColumns = useMemo(() => props.getAllColumns().filter(column => column.getIsVisible()), [props]);
|
|
857
|
+
const visibleColumnsCount = visibleColumns.length;
|
|
858
|
+
const headerGroups = useMemo(() => props.getHeaderGroups(), [props]);
|
|
859
|
+
const totalVisibleColumnsWidth = useMemo(() => {
|
|
860
|
+
return visibleColumns.reduce((sum, column) => sum + column.getSize(), 0);
|
|
861
|
+
}, [visibleColumns]);
|
|
862
|
+
const hasAnyGrowingColumn = useMemo(() => {
|
|
863
|
+
return visibleColumns.some(column => column.columnDef.meta?.shouldGrow === true);
|
|
864
|
+
}, [visibleColumns]);
|
|
865
|
+
const [containWholeTable, setContainWholeTable] = useState(false);
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
const updateContainWholeTable = () => {
|
|
868
|
+
if (tableContainerRef.current) {
|
|
869
|
+
const containerWidth = tableContainerRef.current.offsetWidth;
|
|
870
|
+
setContainWholeTable(totalVisibleColumnsWidth > containerWidth);
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
updateContainWholeTable();
|
|
874
|
+
window.addEventListener("resize", updateContainWholeTable);
|
|
875
|
+
return () => {
|
|
876
|
+
window.removeEventListener("resize", updateContainWholeTable);
|
|
877
|
+
};
|
|
878
|
+
}, [totalVisibleColumnsWidth]);
|
|
879
|
+
const headerOrder = useMemo(() => {
|
|
880
|
+
return props
|
|
881
|
+
.getHeaderGroups()
|
|
882
|
+
.flatMap(group => group.headers)
|
|
883
|
+
.map(header => header.id);
|
|
884
|
+
}, [props]);
|
|
885
|
+
const orderedVisibleColumns = useMemo(() => {
|
|
886
|
+
return headerOrder.map(headerId => visibleColumns.find(col => col.id === headerId)).filter(Boolean);
|
|
887
|
+
}, [headerOrder, visibleColumns]);
|
|
888
|
+
const lastUnpinnedColumnId = useMemo(() => {
|
|
889
|
+
const unpinnedColumns = orderedVisibleColumns.filter(column => column?.getIsPinned() !== "right");
|
|
890
|
+
return unpinnedColumns[unpinnedColumns.length - 1]?.id;
|
|
891
|
+
}, [orderedVisibleColumns]);
|
|
892
|
+
const isLastUnpinned = useCallback((columnId) => columnId === lastUnpinnedColumnId, [lastUnpinnedColumnId]);
|
|
893
|
+
// Add our drag and drop hook with enhanced functionality
|
|
894
|
+
const { getHeaderProps, getDragHandleProps, renderDropIndicator, isCellDropTarget, draggingColumnId, droppedColumnId, getCellProps, } = useColumnDragDrop({
|
|
895
|
+
table: props,
|
|
896
|
+
});
|
|
897
|
+
const offsets = useCellsOffset(headerGroups.map(group => group.headers), draggingColumnId);
|
|
494
898
|
const hasResults = props.getRowModel().rows.length > 0;
|
|
495
|
-
return (jsxs(Card, { className: twMerge("flex flex-col overflow-hidden", props.className), dataTestId: props.dataTestId, children: [props.headerLeftActions || props.headerRightActions ? (jsxs("div", { className: "z-default flex justify-between gap-2 p-2", children: [jsx("div", { className: "flex items-center", children: props.headerLeftActions }), jsx("div", { className: "flex items-center", children: props.headerRightActions })] })) : null, jsx("div", { className: "h-full overflow-x-auto overflow-y-scroll border-b border-t border-
|
|
899
|
+
return (jsxs(Card, { className: twMerge("flex flex-col overflow-hidden", props.className), dataTestId: props.dataTestId, children: [props.headerLeftActions || props.headerRightActions ? (jsxs("div", { className: "z-default flex justify-between gap-2 p-2", children: [jsx("div", { className: "flex items-center gap-2", children: props.headerLeftActions }), jsx("div", { className: "flex items-center gap-2", children: props.headerRightActions })] })) : null, jsx("div", { className: "h-full overflow-x-auto overflow-y-scroll border-b border-t border-neutral-200", onScroll: e => fetchMoreOnBottomReached(e.target), ref: tableContainerRef, children: jsxs(TableRoot, { style: {
|
|
496
900
|
height: hasResults ? "auto" : "100%",
|
|
497
901
|
width: "100%",
|
|
498
902
|
position: "relative",
|
|
499
|
-
}, children: [jsx(Thead, { className: "z-default", children:
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
903
|
+
}, children: [jsx(Thead, { className: "z-default", children: headerGroups.map((headerGroup, index) => {
|
|
904
|
+
return (jsx(Tr, { className: "flex", children: headerGroup.headers.map(header => {
|
|
905
|
+
const tooltipLabel = header.column.columnDef.meta?.tootipLabel ??
|
|
906
|
+
(typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : "");
|
|
907
|
+
const canSort = header.column.getCanSort();
|
|
908
|
+
const offsetData = offsets[index]?.leftOffsets[header.id] || offsets[index]?.rightOffsets[header.id];
|
|
909
|
+
const { offset = 0, pinned = "" } = offsetData ?? {};
|
|
910
|
+
const position = pinned === "right" && !containWholeTable && !hasAnyGrowingColumn ? { position: "absolute" } : {};
|
|
911
|
+
return (createElement(Th, { ...getHeaderProps(header.id, {
|
|
912
|
+
[pinned]: offset,
|
|
913
|
+
...position,
|
|
914
|
+
}), className: cvaTh({
|
|
915
|
+
pinned: pinned || false,
|
|
916
|
+
lastUnpinnedWithoutBorder: isLastUnpinned(header.column.id) && (containWholeTable || hasAnyGrowingColumn),
|
|
917
|
+
isColumnDragging: draggingColumnId === header.id,
|
|
918
|
+
isColumnDropped: droppedColumnId === header.id,
|
|
919
|
+
}), "data-pinned": pinned, key: header.id },
|
|
920
|
+
renderDropIndicator(header.id),
|
|
921
|
+
header.isPlaceholder ? null : (jsxs("div", { ...getDragHandleProps(header.id, header.isPlaceholder, pinned), className: twMerge(cvaColumnHeaderContainer({
|
|
922
|
+
sortable: canSort,
|
|
923
|
+
})), onClick: header.column.getToggleSortingHandler(), children: [jsxs("div", { className: "flex flex-1 items-center gap-1 truncate", children: [header.column.columnDef.header || header.column.columnDef.meta?.subHeader ? (jsxs("span", { className: "subtle min-w-0 text-xs", children: [jsx(TextWithTooltip, { tooltipLabel: tooltipLabel, weight: "bold", children: flexRender(header.column.columnDef.header, header.getContext()) }), header.column.columnDef.meta?.subHeader
|
|
924
|
+
? header.column.columnDef.meta.subHeader
|
|
925
|
+
: null] })) : null, jsx(ColumnSorting, { canSort: canSort, sortingState: header.column.getIsSorted() })] }), jsxs("div", { className: "flex flex-none items-center", children: [props.renderFilterButton && header.column.columnDef.meta?.filterName
|
|
926
|
+
? props.renderFilterButton(header.column.columnDef.meta.filterName)
|
|
927
|
+
: null, jsx(ColumnActions, { header: header, visibleColumnsCount: visibleColumnsCount })] })] })),
|
|
928
|
+
!header.column.columnDef.meta?.shouldGrow && header.column.getCanResize() ? (jsx(ResizeHandle, { isResizing: header.column.getIsResizing(), onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler() })) : null));
|
|
929
|
+
}) }, headerGroup.id));
|
|
930
|
+
}) }), hasResults ? (jsx(Tbody, { className: "text-sm font-normal", style: {
|
|
511
931
|
height: `${getTotalSize()}px`,
|
|
512
932
|
flexGrow: 1,
|
|
513
933
|
}, children: getVirtualItems().map((virtualRow, index) => {
|
|
@@ -516,14 +936,14 @@ const Table = ({ rowHeight = 75, ...props }) => {
|
|
|
516
936
|
return null;
|
|
517
937
|
}
|
|
518
938
|
else {
|
|
519
|
-
return (jsx(Tr, { className: cvaInteractableItem({
|
|
939
|
+
return (jsx(Tr, { className: twMerge("group", cvaInteractableItem({
|
|
520
940
|
cursor: (!!props.onRowClick || row.getCanSelect()) &&
|
|
521
941
|
props.isRowClickable &&
|
|
522
942
|
props.isRowClickable(row.original)
|
|
523
943
|
? "pointer"
|
|
524
944
|
: "default",
|
|
525
945
|
selected: "auto",
|
|
526
|
-
}), dataTestId: `table-body-row-${virtualRow.index}`, layout: "flex", onClick: () => {
|
|
946
|
+
})), dataTestId: `table-body-row-${virtualRow.index}`, layout: "flex", onClick: () => {
|
|
527
947
|
if (props.onRowClick) {
|
|
528
948
|
props.onRowClick(row);
|
|
529
949
|
}
|
|
@@ -534,18 +954,32 @@ const Table = ({ rowHeight = 75, ...props }) => {
|
|
|
534
954
|
height: `${virtualRow.size}px`,
|
|
535
955
|
transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`,
|
|
536
956
|
}, children: row.getVisibleCells().map(cell => {
|
|
537
|
-
|
|
957
|
+
// Processing the cell identifier:
|
|
958
|
+
// 1. Remove the segment enclosed by "___" (e.g., "___Asset___").
|
|
959
|
+
// 2. Remove the initial segment of the string, including the first "_" (e.g., "25_" or "7fa7655d-3bc2-402b-99dd-5ea1f64c3b96_").
|
|
960
|
+
// The final value will be the key part of the identifier, e.g., "DEVICE_SERIAL_NUMBER".
|
|
961
|
+
const headerId = cell.id.replace(/___[^_]+___/, "").replace(/^[^_]*_/, "");
|
|
962
|
+
const offsetData = offsets[0]?.leftOffsets[headerId] || offsets[0]?.rightOffsets[headerId];
|
|
963
|
+
const { offset = 0, pinned = "" } = offsetData ?? {};
|
|
964
|
+
const position = pinned === "right" && !containWholeTable && !hasAnyGrowingColumn
|
|
965
|
+
? { position: "absolute", height: "-webkit-fill-available" }
|
|
966
|
+
: {};
|
|
967
|
+
return (createElement(Td, { ...getCellProps(cell.column.id, {
|
|
968
|
+
[pinned]: offset,
|
|
969
|
+
...position,
|
|
970
|
+
}), className: cvaTd({
|
|
971
|
+
pinned: pinned || false,
|
|
972
|
+
lastUnpinnedWithoutBorder: isLastUnpinned(cell.column.id) && (containWholeTable || hasAnyGrowingColumn),
|
|
973
|
+
isColumnDragging: draggingColumnId === cell.column.id,
|
|
974
|
+
isColumnDropped: droppedColumnId === cell.column.id,
|
|
975
|
+
}), "data-pinned": pinned, key: cell.id, onClick: event => {
|
|
538
976
|
// prevent onRowClick action when clicking on a "selectable" cell with checkbox
|
|
539
977
|
if (props.selectionColId && cell.column.id === props.selectionColId) {
|
|
540
978
|
event.stopPropagation();
|
|
541
979
|
}
|
|
542
|
-
},
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
maxWidth: cell.column.columnDef.maxSize,
|
|
546
|
-
textAlign: cell.column.columnDef.meta?.alignment || "left",
|
|
547
|
-
flexGrow: cell.column.columnDef.meta?.shouldGrow ? 1 : 0,
|
|
548
|
-
}, children: jsx("div", { className: "grid h-full items-center", children: flexRender(cell.column.columnDef.cell, cell.getContext()) }) }, cell.id));
|
|
980
|
+
} },
|
|
981
|
+
isCellDropTarget(cell.column.id) && renderDropIndicator(cell.column.id),
|
|
982
|
+
jsx("div", { className: "grid h-full items-center p-0", children: flexRender(cell.column.columnDef.cell, cell.getContext()) })));
|
|
549
983
|
}) }, row.id));
|
|
550
984
|
}
|
|
551
985
|
}) })) : (jsx("tbody", { className: cvaTBody({ emptyState: !props.loading && !props.noDataMessage }), children: jsx("tr", { children: jsx("td", { className: "b-0", children: props.loading ? (jsx(Spinner, { centering: "centered", containerClassName: "absolute inset-0" })) : props.noDataMessage ? (props.noDataMessage) : (jsx(EmptyState, { className: "absolute inset-0", description: t("table.noResults"), image: "SEARCH_DOCUMENT", ...props.emptyState })) }) }) }))] }) }), props.hideFooter ? null : (jsxs("div", { className: "flex items-center p-2", children: [jsx("div", { className: "whitespace-nowrap text-xs font-medium text-neutral-600", children: t(props.pagination?.pageInfo?.isCountCapped ? "table.pagination.full.capped" : "table.pagination.full", {
|
|
@@ -563,6 +997,71 @@ const cvaTBody = cvaMerge(["min-h-[40dvh]"], {
|
|
|
563
997
|
emptyState: false,
|
|
564
998
|
},
|
|
565
999
|
});
|
|
1000
|
+
const cvaColumnHeaderContainer = cvaMerge(["flex", "min-w-0", "w-full", "items-center", "self-center", "gap-2", "overflow-hidden"], {
|
|
1001
|
+
variants: {
|
|
1002
|
+
sortable: {
|
|
1003
|
+
true: "cursor-pointer select-none",
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
defaultVariants: {
|
|
1007
|
+
sortable: false,
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
const cvaTh = cvaMerge(["flex", "relative"], {
|
|
1011
|
+
variants: {
|
|
1012
|
+
pinned: {
|
|
1013
|
+
left: "sticky left-0 z-[1] bg-neutral-100",
|
|
1014
|
+
right: "sticky right-0 z-[1] border-r-0 bg-neutral-100",
|
|
1015
|
+
false: "relative",
|
|
1016
|
+
},
|
|
1017
|
+
lastUnpinnedWithoutBorder: {
|
|
1018
|
+
true: "border-r-0",
|
|
1019
|
+
false: "",
|
|
1020
|
+
},
|
|
1021
|
+
isColumnDragging: {
|
|
1022
|
+
true: "dragging-column",
|
|
1023
|
+
false: "",
|
|
1024
|
+
},
|
|
1025
|
+
isColumnDropped: {
|
|
1026
|
+
true: "column-dropped",
|
|
1027
|
+
false: "",
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
defaultVariants: {
|
|
1031
|
+
pinned: false,
|
|
1032
|
+
isColumnDragging: false,
|
|
1033
|
+
isColumnDropped: false,
|
|
1034
|
+
lastUnpinnedWithoutBorder: false,
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
const sharedPinnedStyles = "sticky z-[1] group-has-[:checked]:bg-primary-50 group-hover:has-[:checked]:bg-primary-50 group-focus-within:has-[:checked]:bg-primary-50 bg-white group-hover:bg-neutral-100";
|
|
1038
|
+
const cvaTd = cvaMerge(["relative"], {
|
|
1039
|
+
variants: {
|
|
1040
|
+
pinned: {
|
|
1041
|
+
left: `${sharedPinnedStyles} left-0`,
|
|
1042
|
+
right: `${sharedPinnedStyles} border-r-0`,
|
|
1043
|
+
false: "",
|
|
1044
|
+
},
|
|
1045
|
+
lastUnpinnedWithoutBorder: {
|
|
1046
|
+
true: "border-r-0",
|
|
1047
|
+
false: "",
|
|
1048
|
+
},
|
|
1049
|
+
isColumnDragging: {
|
|
1050
|
+
true: "dragging-column",
|
|
1051
|
+
false: "",
|
|
1052
|
+
},
|
|
1053
|
+
isColumnDropped: {
|
|
1054
|
+
true: "column-dropped",
|
|
1055
|
+
false: "",
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
defaultVariants: {
|
|
1059
|
+
pinned: false,
|
|
1060
|
+
isColumnDragging: false,
|
|
1061
|
+
isColumnDropped: false,
|
|
1062
|
+
lastUnpinnedWithoutBorder: false,
|
|
1063
|
+
},
|
|
1064
|
+
});
|
|
566
1065
|
|
|
567
1066
|
/**
|
|
568
1067
|
* Creates and returns a memoized instance of a columnHelper
|
|
@@ -581,11 +1080,13 @@ const useColumnHelper = () => {
|
|
|
581
1080
|
* @returns {TableOptionsProps<TData>} An object containing the React Table instance and associated state management functions.
|
|
582
1081
|
*/
|
|
583
1082
|
const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProps }) => {
|
|
1083
|
+
const stableColumns = useMemo(() => JSON.stringify(columns), [columns]);
|
|
584
1084
|
const updatedInitialColumnVisibility = useMemo(() => {
|
|
1085
|
+
const columnsList = JSON.parse(stableColumns);
|
|
585
1086
|
const initialStateColumnVisibility = initialState?.columnVisibility;
|
|
586
1087
|
const resultFromInitialState = initialStateColumnVisibility
|
|
587
|
-
? Object.fromEntries(
|
|
588
|
-
.map(column => {
|
|
1088
|
+
? Object.fromEntries(columnsList
|
|
1089
|
+
.map((column) => {
|
|
589
1090
|
if (column.id && initialStateColumnVisibility[column.id] !== undefined) {
|
|
590
1091
|
return [column.id, initialStateColumnVisibility[column.id]];
|
|
591
1092
|
}
|
|
@@ -593,26 +1094,45 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
593
1094
|
})
|
|
594
1095
|
.filter(nonNullable))
|
|
595
1096
|
: {};
|
|
596
|
-
|
|
1097
|
+
columnsList.forEach((column) => {
|
|
597
1098
|
if (column.id && resultFromInitialState[column.id] === undefined) {
|
|
598
|
-
resultFromInitialState[column.id] = column.meta?.hiddenByDefault
|
|
1099
|
+
resultFromInitialState[column.id] = column.meta?.hiddenByDefault !== true;
|
|
599
1100
|
}
|
|
600
1101
|
});
|
|
601
1102
|
return resultFromInitialState;
|
|
602
|
-
}, [
|
|
1103
|
+
}, [stableColumns, initialState?.columnVisibility]);
|
|
603
1104
|
const updatedInitialColumnOrder = useMemo(() => {
|
|
604
1105
|
const resultFromInitialState = initialState?.columnOrder || [];
|
|
605
|
-
|
|
1106
|
+
JSON.parse(stableColumns).forEach((column) => {
|
|
606
1107
|
if (column.id && !resultFromInitialState.includes(column.id)) {
|
|
607
1108
|
resultFromInitialState.push(column.id);
|
|
608
1109
|
}
|
|
609
1110
|
});
|
|
610
1111
|
return resultFromInitialState;
|
|
611
|
-
}, [
|
|
612
|
-
const
|
|
1112
|
+
}, [stableColumns, initialState?.columnOrder]);
|
|
1113
|
+
const stableUpdatedInitialColumnPinning = useMemo(() => {
|
|
1114
|
+
const pinningState = { left: [], right: [] };
|
|
1115
|
+
JSON.parse(stableColumns).forEach((column) => {
|
|
1116
|
+
if (column.id) {
|
|
1117
|
+
if (column.meta?.pinned === "left") {
|
|
1118
|
+
if (pinningState.left) {
|
|
1119
|
+
pinningState.left.push(column.id);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
else if (column.meta?.pinned === "right") {
|
|
1123
|
+
if (pinningState.right) {
|
|
1124
|
+
pinningState.right.push(column.id);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
return pinningState;
|
|
1130
|
+
}, [stableColumns]);
|
|
1131
|
+
const [columnVisibility, setColumnVisibility] = useState(reactTableProps.state?.columnVisibility || updatedInitialColumnVisibility);
|
|
613
1132
|
const [columnOrder, setColumnOrder] = useState(reactTableProps.state?.columnOrder || updatedInitialColumnOrder);
|
|
614
1133
|
const [sorting, setSorting] = useState(reactTableProps.state?.sorting || initialState?.sorting || []);
|
|
615
1134
|
const [columnSizing, setColumnSizing] = useState(reactTableProps.state?.columnSizing || initialState?.columnSizing || {});
|
|
1135
|
+
const [columnPinning, setColumnPinning] = useState(reactTableProps.state?.columnPinning || stableUpdatedInitialColumnPinning);
|
|
616
1136
|
useEffect(() => {
|
|
617
1137
|
if (initialState && objectKeys(initialState).length > 0) {
|
|
618
1138
|
setColumnVisibility(updatedInitialColumnVisibility);
|
|
@@ -620,19 +1140,34 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
620
1140
|
setSorting(initialState.sorting || []);
|
|
621
1141
|
setColumnSizing(initialState.columnSizing || {});
|
|
622
1142
|
}
|
|
623
|
-
}, [initialState, updatedInitialColumnOrder, updatedInitialColumnVisibility]);
|
|
1143
|
+
}, [initialState, updatedInitialColumnOrder, stableUpdatedInitialColumnPinning, updatedInitialColumnVisibility]);
|
|
1144
|
+
useEffect(() => {
|
|
1145
|
+
const mergedLeft = initialState?.columnPinning?.left?.length
|
|
1146
|
+
? [...initialState.columnPinning.left]
|
|
1147
|
+
: [...(stableUpdatedInitialColumnPinning.left || [])];
|
|
1148
|
+
const mergedRight = initialState?.columnPinning?.right?.length
|
|
1149
|
+
? [...initialState.columnPinning.right]
|
|
1150
|
+
: [...(stableUpdatedInitialColumnPinning.right || [])];
|
|
1151
|
+
const filteredRight = mergedRight.filter(column => !mergedLeft.includes(column));
|
|
1152
|
+
const mergedPinningState = {
|
|
1153
|
+
left: mergedLeft,
|
|
1154
|
+
right: filteredRight,
|
|
1155
|
+
};
|
|
1156
|
+
setColumnPinning(mergedPinningState);
|
|
1157
|
+
}, [initialState?.columnPinning, stableUpdatedInitialColumnPinning]);
|
|
624
1158
|
const state = useMemo(() => {
|
|
625
1159
|
return {
|
|
626
1160
|
sorting,
|
|
627
1161
|
columnVisibility,
|
|
628
1162
|
columnOrder,
|
|
629
1163
|
columnSizing,
|
|
1164
|
+
columnPinning,
|
|
630
1165
|
...reactTableProps.state,
|
|
631
1166
|
};
|
|
632
|
-
}, [sorting, columnVisibility, columnOrder, columnSizing, reactTableProps.state]);
|
|
1167
|
+
}, [sorting, columnVisibility, columnOrder, columnSizing, columnPinning, reactTableProps.state]);
|
|
633
1168
|
const table = useReactTable({
|
|
634
1169
|
manualSorting: true,
|
|
635
|
-
enableSortingRemoval:
|
|
1170
|
+
enableSortingRemoval: true,
|
|
636
1171
|
columnResizeMode: "onChange",
|
|
637
1172
|
getSortedRowModel: getSortedRowModel(),
|
|
638
1173
|
getCoreRowModel: getCoreRowModel(),
|
|
@@ -653,6 +1188,10 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
653
1188
|
setSorting(value);
|
|
654
1189
|
reactTableProps.onSortingChange?.(value);
|
|
655
1190
|
},
|
|
1191
|
+
onColumnPinningChange: value => {
|
|
1192
|
+
setColumnPinning(value);
|
|
1193
|
+
reactTableProps.onColumnPinningChange?.(value);
|
|
1194
|
+
},
|
|
656
1195
|
columns,
|
|
657
1196
|
state,
|
|
658
1197
|
});
|
|
@@ -724,11 +1263,12 @@ const useTableSelection = ({ data, idKey, defaultSelectedIds, enableRowSelection
|
|
|
724
1263
|
if (!hasRowSelection) {
|
|
725
1264
|
return null;
|
|
726
1265
|
}
|
|
727
|
-
|
|
1266
|
+
const isRowSelectionDisabled = !table.options.enableRowSelection || !data.length;
|
|
1267
|
+
return (jsx(Checkbox, { className: "ml-2.5",
|
|
728
1268
|
checked: table.getIsAllRowsSelected(),
|
|
729
1269
|
indeterminate: table.getIsSomeRowsSelected(),
|
|
730
1270
|
onChange: table.getToggleAllRowsSelectedHandler(),
|
|
731
|
-
disabled:
|
|
1271
|
+
disabled: isRowSelectionDisabled }));
|
|
732
1272
|
},
|
|
733
1273
|
cell: ({ row, table }) => {
|
|
734
1274
|
const hasRowSelection = table.getState().rowSelection !== undefined;
|
|
@@ -747,10 +1287,12 @@ const useTableSelection = ({ data, idKey, defaultSelectedIds, enableRowSelection
|
|
|
747
1287
|
enableSorting: false,
|
|
748
1288
|
enableResizing: false,
|
|
749
1289
|
enableHiding: false,
|
|
1290
|
+
enablePinning: false,
|
|
750
1291
|
size: 70,
|
|
751
1292
|
meta: {
|
|
752
1293
|
alignment: "center",
|
|
753
1294
|
columnFilterLabel: t("table.selection.label"),
|
|
1295
|
+
pinned: "left",
|
|
754
1296
|
},
|
|
755
1297
|
}), [columnHelper, data.length, idKey, t]);
|
|
756
1298
|
const selectionTableState = useMemo(() => ({ rowSelection }), [rowSelection]);
|