diff --git a/src/index.css b/src/index.css index 441b9f7..51225e0 100644 --- a/src/index.css +++ b/src/index.css @@ -561,6 +561,26 @@ } /* Editor Styles */ +/* Thicker horizontal scrollbar for data tables */ +.data-table-scroll { + scrollbar-width: thin; + scrollbar-color: #cbd5e1 #f1f5f9; +} +.data-table-scroll::-webkit-scrollbar { + height: 8px; +} +.data-table-scroll::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 4px; +} +.data-table-scroll::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} +.data-table-scroll::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + input /* Change the white to any color */ input:-webkit-autofill, diff --git a/src/misc/tanstack-table/Table.tsx b/src/misc/tanstack-table/Table.tsx index 3a2cc36..e6166e5 100644 --- a/src/misc/tanstack-table/Table.tsx +++ b/src/misc/tanstack-table/Table.tsx @@ -29,6 +29,7 @@ import { } from '@dnd-kit/sortable'; import { ColumnDef, + ColumnSizingState, flexRender, getCoreRowModel, getPaginationRowModel, @@ -77,19 +78,28 @@ function getInitialColumnOrder( fixedStartColIds: string[], fixedEndColIds: string[] ): string[] { + const allColIds = new Set( + columns + .map((col) => { + if ('id' in col && col.id) return col.id as string; + if ('accessorKey' in col && col.accessorKey) + return col.accessorKey as string; + return ''; + }) + .filter(Boolean) + ); + const validFixedEnd = fixedEndColIds.filter((id) => allColIds.has(id)); + if (initialColumnOrder && initialColumnOrder.length > 0) { - return initialColumnOrder; + // Strip fixedStart and fixedEnd cols from provided order, + // then always prepend/append them so they stay pinned. + const base = initialColumnOrder.filter( + (id) => !fixedStartColIds.includes(id) && !fixedEndColIds.includes(id) + ); + return [...fixedStartColIds, ...base, ...validFixedEnd]; } - const colIds = columns - .map((col) => { - if ('id' in col && col.id) return col.id as string; - if ('accessorKey' in col && col.accessorKey) - return col.accessorKey as string; - return ''; - }) - .filter(Boolean); - + const colIds = [...allColIds]; const draggable = colIds.filter( (id) => !fixedStartColIds.includes(id) && !fixedEndColIds.includes(id) ); @@ -134,7 +144,10 @@ export function useDataTable({ // Sync internal columnOrder state when initialColumnOrder prop changes externally useEffect(() => { if (initialColumnOrder && initialColumnOrder.length > 0) { - setColumnOrder(initialColumnOrder); + const base = initialColumnOrder.filter( + (id) => !fixedStartColIds.includes(id) && !fixedEndColIds.includes(id) + ); + setColumnOrder([...fixedStartColIds, ...base, ...fixedEndColIds]); } }, [initialColumnOrder]); @@ -157,6 +170,39 @@ export function useDataTable({ const scrollPositionRef = useRef({ scrollTop: 0, scrollLeft: 0 }); const shouldRestoreScrollRef = useRef(false); + /** + * Column Resizing — persisted to localStorage keyed by column IDs + */ + const columnSizingKey = useRef( + 'col-sizing:' + + columns + .map( + (col) => + ('id' in col && col.id) || + ('accessorKey' in col && String(col.accessorKey)) || + '' + ) + .filter(Boolean) + .join(',') + ).current; + + const [columnSizing, setColumnSizing] = useState(() => { + try { + const saved = localStorage.getItem(columnSizingKey); + return saved ? JSON.parse(saved) : {}; + } catch { + return {}; + } + }); + + useEffect(() => { + try { + localStorage.setItem(columnSizingKey, JSON.stringify(columnSizing)); + } catch (e) { + /* localStorage unavailable */ + } + }, [columnSizing, columnSizingKey]); + /** * Sort */ @@ -205,12 +251,19 @@ export function useDataTable({ // Also maintain scroll position on render when we have a saved position useLayoutEffect(() => { - if (scrollContainerRef.current && (scrollPositionRef.current.scrollLeft > 0 || scrollPositionRef.current.scrollTop > 0)) { + if ( + scrollContainerRef.current && + (scrollPositionRef.current.scrollLeft > 0 || + scrollPositionRef.current.scrollTop > 0) + ) { const element = scrollContainerRef.current; const { scrollLeft, scrollTop } = scrollPositionRef.current; // Only restore if position has been reset - if (element.scrollLeft !== scrollLeft || element.scrollTop !== scrollTop) { + if ( + element.scrollLeft !== scrollLeft || + element.scrollTop !== scrollTop + ) { element.scrollLeft = scrollLeft; element.scrollTop = 0; } @@ -446,12 +499,16 @@ export function useDataTable({ manualPagination: true, onRowSelectionChange: setRowSelection, enableRowSelection: true, + enableColumnResizing: true, + columnResizeMode: 'onChange', + onColumnSizingChange: setColumnSizing, ...tableParams, state: { columnOrder, sorting, pagination, rowSelection, + columnSizing, ...tableParams?.state, }, }); @@ -622,164 +679,238 @@ export function useDataTable({ /** * Render Data table Component */ - const CustomDataTable = () => ( -
- {includeLoading && !data?.length ? ( -
- -
- ) : ( -
rest)( - itemProps?.tableWrapper || {} - )} - className={clsx( - 'rounded-md border', - { - 'max-h-[65vh] min-h-[0px] overflow-y-auto': - itemProps?.tableWrapper?.enableStickyHeader, - }, - 'overflow-x-auto', - itemProps?.tableWrapper?.className - )} - > - { + const tableMinWidth = table + .getVisibleLeafColumns() + .reduce((acc, col) => acc + col.getSize(), 0); + const totalColWidth = Math.max(tableMinWidth, 1); + + return ( +
+ {includeLoading && !data?.length ? ( +
+ +
+ ) : ( +
rest)( + itemProps?.tableWrapper || {} + )} + className={clsx( + 'relative rounded-md border', + { + 'max-h-[65vh] min-h-[0px] overflow-y-auto': + itemProps?.tableWrapper?.enableStickyHeader, + }, + 'overflow-x-auto data-table-scroll', + itemProps?.tableWrapper?.className + )} > - - {table.getRowModel().rows?.length ? ( - - {table.getHeaderGroups().map((headerGroup) => ( - - - {headerGroup.headers.map((header) => { - return ( - - ); - })} - - - ))} - - ) : null} - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => - CustomTableRow ? ( - handleRowClick({ event, row })} - data-state={row.getIsSelected() && 'selected'} - data-testid={'data-table-row-' + row.id} - row={row} - {...bodyRowProps(row)} - className={clsx(bodyRowProps(row)?.className)} - > - - - ) : ( + + {table.getHeaderGroups().map((headerGroup) => ( handleRowClick({ event, row })} - {...bodyRowProps(row)} - className={clsx(bodyRowProps(row)?.className)} - data-state={row.getIsSelected() && 'selected'} - data-testid={'data-table-row-' + row.id} + data-testid={'data-header-row-' + headerGroup.id} + key={headerGroup.id} + {...itemProps?.tableHeaderRow} + className={clsx( + 'hover:!bg-transparent', + itemProps?.tableHeaderRow?.className + )} > - + + {(() => { + // Get only visible headers (filter out placeholders if needed) + const visibleHeaders = headerGroup.headers.filter( + (header) => !header.isPlaceholder + ); + + // Compute cumulative right offsets for fixedEndColIds sticky columns + const stickyHeaderRightOffsets: Record< + string, + number + > = {}; + let headerCumulativeRight = 0; + for ( + let i = visibleHeaders.length - 1; + i >= 0; + i-- + ) { + const h = visibleHeaders[i]; + if (fixedEndColIds.includes(h.column.id)) { + stickyHeaderRightOffsets[h.column.id] = + headerCumulativeRight; + const colWidth = + h.column.columnDef.maxSize !== undefined && + h.column.columnDef.maxSize < + Number.MAX_SAFE_INTEGER + ? h.getSize() + : Math.max( + h.column.columnDef.minSize ?? 20, + 80 + ); + headerCumulativeRight += colWidth; + } + } + + return visibleHeaders.map((header, index) => { + const isLastVisibleColumn = + index === visibleHeaders.length - 1; + const nextHeader = visibleHeaders[index + 1]; + const isBeforeFixedEnd = + !!nextHeader && + fixedEndColIds.includes(nextHeader.column.id); + + return ( + + ); + }); + })()} + + ))} + + ) : null} + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => + CustomTableRow ? ( + handleRowClick({ event, row })} + data-state={row.getIsSelected() && 'selected'} + data-testid={'data-table-row-' + row.id} + row={row} + {...bodyRowProps(row)} + className={clsx(bodyRowProps(row)?.className)} + > + + + ) : ( + handleRowClick({ event, row })} + {...bodyRowProps(row)} + className={clsx(bodyRowProps(row)?.className)} + data-state={row.getIsSelected() && 'selected'} + data-testid={'data-table-row-' + row.id} + > + + + ) ) - ) - ) : ( - - - {noRecordFoundMessage} - - - )} - -
- - - - - -
- )} - {showPagination && table.getRowModel().rows?.length ? ( - - ) : null} -
- ); + + {noRecordFoundMessage} + + + )} + + + + + + +
+
+ )} + {showPagination && table.getRowModel().rows?.length ? ( + + ) : null} +
+ ); + }; type TableRowCellProps = { row: Row; @@ -796,32 +927,59 @@ export function useDataTable({ itemProps, dropIndicatorInstruction, }: TableRowCellProps) => { + const visibleCells = row.getVisibleCells(); + + // Compute cumulative right offsets for fixedEndColIds sticky columns + const stickyCellRightOffsets: Record = {}; + let cellCumulativeRight = 0; + for (let i = visibleCells.length - 1; i >= 0; i--) { + const c = visibleCells[i]; + if (fixedEndColIds.includes(c.column.id)) { + stickyCellRightOffsets[c.column.id] = cellCumulativeRight; + const colWidth = + c.column.columnDef.maxSize !== undefined && + c.column.columnDef.maxSize < Number.MAX_SAFE_INTEGER + ? c.column.getSize() + : Math.max(c.column.columnDef.minSize ?? 20, 80); + cellCumulativeRight += colWidth; + } + } + return ( <>
- {row.getVisibleCells().map((cell, index) => { + {visibleCells.map((cell, index) => { const isFirstCell = index === 0; + const isLastCell = index === visibleCells.length - 1; + const isResizingThisCell = + !isLastCell && + table.getState().columnSizingInfo.isResizingColumn === + cell.column.id; return ( {/* Inject the draggable icon ONLY in the first cell when renderDraggableIcon is true */} @@ -992,4 +1150,4 @@ export function useDataTable({ handleSelectAll, }, }; -} \ No newline at end of file +} diff --git a/src/misc/tanstack-table/TableComponents.tsx b/src/misc/tanstack-table/TableComponents.tsx index f20aea4..8cc35cb 100644 --- a/src/misc/tanstack-table/TableComponents.tsx +++ b/src/misc/tanstack-table/TableComponents.tsx @@ -3,7 +3,7 @@ import { CSS } from '@dnd-kit/utilities'; import { flexRender, Header } from '@tanstack/react-table'; import clsx from 'clsx'; -import React from 'react'; +import React, { useState } from 'react'; import { HiMiniChevronDown, HiMiniChevronUp } from 'react-icons/hi2'; import { DataTableProps } from './type'; @@ -129,12 +129,16 @@ const DraggableColumnHeader = ({ header, itemProps, isDraggable, + isResizable = true, saveScrollPositionBeforeSort, + isLastVisibleColumn, }: { header: Header; itemProps: DataTableProps['itemProps']; isDraggable: boolean; + isResizable?: boolean; saveScrollPositionBeforeSort?: () => void; + isLastVisibleColumn?: boolean; }) => { const { attributes, @@ -149,27 +153,30 @@ const DraggableColumnHeader = ({ disabled: !isDraggable, }); + const [isResizeHovered, setIsResizeHovered] = useState(false); + const style: React.CSSProperties = { opacity: isDragging ? 0.8 : 1, position: 'relative', transform: CSS.Translate.toString(transform), whiteSpace: 'nowrap', - width: `${header.column.columnDef.size}px`, - minWidth: `${header.column.columnDef.minSize}px`, - maxWidth: `${header.column.columnDef.maxSize}px`, + minWidth: `${header.column.columnDef.minSize ?? 20}px`, transition, - zIndex: isDragging ? 10 : 1, + zIndex: isDragging + ? 10 + : header.column.getIsResizing() || isResizeHovered + ? 3 + : 1, }; return ( td]:justify-center', { // Uncomment this line if you want to show background when dragging @@ -177,11 +184,12 @@ const DraggableColumnHeader = ({ 'bg-blue-50/20 border-l-2 border-l-surface-cta': isOver && !isDragging, 'hover:bg-gray-50': isDraggable && !isDragging && !isOver, + 'border-r border-gray-200': !isLastVisibleColumn, }, itemProps?.tableHead?.className )} > - ({ } }} className={clsx( - 'flex items-center gap-2 !p-0 font-semibold text-text-pri w-full', + 'flex items-center gap-2 font-semibold text-text-pri w-full', { 'cursor-pointer select-none': header.column.getCanSort() && !isDragging, @@ -201,8 +209,6 @@ const DraggableColumnHeader = ({ 'cursor-grabbing': isDragging, } )} - // for checked commented this code in future we remove this - // style={itemProps?.tableHead?.style} > {header.isPlaceholder ? null @@ -228,7 +234,43 @@ const DraggableColumnHeader = ({ />
) : null} - + + {!isLastVisibleColumn && isResizable && ( +
e.stopPropagation()} + onMouseEnter={() => setIsResizeHovered(true)} + onMouseLeave={() => setIsResizeHovered(false)} + title='Adjust column width' + style={{ + position: 'absolute', + right: '0', + top: 0, + bottom: 0, + width: '10px', + zIndex: 10, + cursor: 'col-resize', + userSelect: 'none', + touchAction: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + }} + > +
+
+ )} ); };