@trackunit/react-table 1.3.138 → 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.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, cvaInteractableItem, Card, Spinner, EmptyState } from '@trackunit/react-components';
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, useState } from 'react';
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, SortIndicator, ResizeHandle, Tbody, Td } from '@trackunit/react-table-base-components';
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
- const result = columnOrder && (columnOrder.length || 0) > 0
306
- ? columns.sort((a, b) => columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id))
307
- : columns;
308
- return result;
309
- }, [columnOrder, columns]);
310
- const getColumnButtonText = () => {
311
- if (numberOfHiddenColumns > 1) {
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
- if (numberOfHiddenColumns === 1) {
315
- return t("table.columnFilters.hiddenColumnCount", { count: numberOfHiddenColumns });
316
- }
317
- return t("table.columnFilters.columns");
318
- };
319
- 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: "ghost-neutral", children: jsx("span", { className: "hidden sm:block", children: getColumnButtonText() }) }) }), jsx(PopoverContent, { children: () => (jsxs(MenuList, { className: "relative max-h-[55vh] w-72 overflow-y-auto pr-4", children: [jsxs("div", { className: "mb-2 flex items-center justify-between", children: [jsx(Text, { size: "small", subtle: true, children: t("table.columnFilters.title") }), jsx(Button, { className: "p-0", onClick: () => resetHiddenColumns(), size: "small", variant: "ghost-neutral", children: t("layout.actions.reset") })] }), jsx(ColumnFiltersDragAndDrop, { columns: sortedColumns, defaultColumnOrder: defaultColumnOrder, onUserEvent: onUserEvent, setColumnOrder: setColumnOrder })] })) })] }) }) }));
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, defaultColumnOrder, }) => {
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 firstElement = defaultColumnOrder[0];
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, defaultColumnOrder]);
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
- return (jsx(ColumnFilterItem, { disabled: !column.getCanHide(), id: column.id, index: index, moveColumn: moveColumn, name: columnDef.meta?.columnFilterLabel ?? columnDef.header?.toString() ?? "", onCancelDrop: onCancelColumDrop, onDrop: onColumDrop, onToggle: (toggled, event) => {
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: column.getIsVisible() }, column.id));
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: true,
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: false }), 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 })] })] }));
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: "ghost-neutral", children: jsx("span", { className: "hidden sm: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") })] }) }) })] }) }));
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 = 75, ...props }) => {
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-gray-300", onScroll: e => fetchMoreOnBottomReached(e.target), ref: tableContainerRef, children: jsxs(TableRoot, { style: {
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: props.getHeaderGroups().map(headerGroup => (jsx(Tr, { className: "flex", children: headerGroup.headers.map(header => {
500
- const tooltipLabel = header.column.columnDef.meta?.tootipLabel ??
501
- (typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : "");
502
- return (jsxs(Th, { className: "relative", style: {
503
- width: header.getSize(),
504
- minWidth: header.column.columnDef.minSize,
505
- maxWidth: header.column.columnDef.maxSize,
506
- textAlign: header.column.columnDef.meta?.alignment || "left",
507
- flexGrow: header.column.columnDef.meta?.shouldGrow ? 1 : 0,
508
- }, tooltipLabel: tooltipLabel, children: [header.isPlaceholder ? null : (jsxs("div", { className: `${header.column.getCanSort() ? "cursor-pointer select-none flex pr-3" : "flex"} items-center gap-2 py-2 overflow-hidden w-full`,
509
- onClick: header.column.getToggleSortingHandler(), children: [jsxs("span", { className: "overflow-hidden text-ellipsis whitespace-nowrap", children: [flexRender(header.column.columnDef.header, header.getContext()), header.column.columnDef.meta?.subHeader ? (jsx(Text, { size: "small", subtle: true, children: header.column.columnDef.meta.subHeader })) : null] }), header.column.getCanSort() ? (jsx(SortIndicator, { sortingState: header.column.getIsSorted() })) : null] })), !header.column.columnDef.meta?.shouldGrow && header.column.getCanResize() ? (jsx(ResizeHandle, { isResizing: header.column.getIsResizing(), onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler() })) : null] }, header.id));
510
- }) }, headerGroup.id))) }), hasResults ? (jsx(Tbody, { className: "text-sm font-normal", style: {
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
- return (jsx(Td, { onClick: event => {
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
- }, style: {
543
- width: cell.column.getSize(),
544
- minWidth: cell.column.columnDef.minSize,
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(columns
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
- columns.forEach(column => {
1097
+ columnsList.forEach((column) => {
597
1098
  if (column.id && resultFromInitialState[column.id] === undefined) {
598
- resultFromInitialState[column.id] = column.meta?.hiddenByDefault === true ? false : true;
1099
+ resultFromInitialState[column.id] = column.meta?.hiddenByDefault !== true;
599
1100
  }
600
1101
  });
601
1102
  return resultFromInitialState;
602
- }, [columns, initialState?.columnVisibility]);
1103
+ }, [stableColumns, initialState?.columnVisibility]);
603
1104
  const updatedInitialColumnOrder = useMemo(() => {
604
1105
  const resultFromInitialState = initialState?.columnOrder || [];
605
- columns.forEach(column => {
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
- }, [columns, initialState?.columnOrder]);
612
- const [columnVisibility, setColumnVisibility] = useState(reactTableProps.state?.columnVisibility || updatedInitialColumnVisibility || {});
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: false,
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
- return (jsx(Checkbox, { className: "ml-4",
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: !data.length }));
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]);