mirror of https://github.com/buster-so/buster.git
resize colors
This commit is contained in:
parent
996115f4d5
commit
8d11b86d6d
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
import { Cell, flexRender } from '@tanstack/react-table';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CELL_HEIGHT } from './constants';
|
||||
|
||||
interface DataGridCellProps {
|
||||
cell: Cell<Record<string, string | number | Date | null>, unknown>;
|
||||
}
|
||||
|
||||
//DO NOT MEMOIZE CELL
|
||||
export const DataGridCell: React.FC<DataGridCellProps> = ({ cell }) => {
|
||||
return (
|
||||
<td
|
||||
className={cn(
|
||||
'relative flex items-center border-r px-2 last:border-r-0',
|
||||
cell.column.getIsResizing() && 'bg-primary/3'
|
||||
)}
|
||||
style={{ width: cell.column.getSize(), height: CELL_HEIGHT }}>
|
||||
<div className="truncate">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
|
||||
|
||||
{cell.column.getIsResizing() && (
|
||||
<span className="bg-primary absolute inset-y-0 -right-0.5 z-10 -my-1 h-[105%] w-1" />
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
DataGridCell.displayName = 'DataGridCell';
|
|
@ -0,0 +1,297 @@
|
|||
import React, { CSSProperties, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
useDraggable,
|
||||
useDroppable,
|
||||
DragStartEvent,
|
||||
DragOverEvent,
|
||||
DragEndEvent,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
KeyboardSensor,
|
||||
useSensors,
|
||||
useSensor
|
||||
} from '@dnd-kit/core';
|
||||
import { Header, Table } from '@tanstack/react-table';
|
||||
import { flexRender } from '@tanstack/react-table';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
|
||||
import { pointerWithin } from '@dnd-kit/core';
|
||||
import { CaretDown, CaretUp } from '../../../icons/NucleoIconFilled';
|
||||
import { HEADER_HEIGHT } from './constants';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
|
||||
interface DraggableHeaderProps {
|
||||
header: Header<Record<string, string | number | Date | null>, unknown>;
|
||||
resizable: boolean;
|
||||
sortable: boolean;
|
||||
overTargetId: string | null;
|
||||
}
|
||||
|
||||
const DraggableHeader: React.FC<DraggableHeaderProps> = React.memo(
|
||||
({ header, sortable, resizable, overTargetId }) => {
|
||||
// Set up dnd-kit's useDraggable for this header cell
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
isDragging,
|
||||
setNodeRef: setDragNodeRef
|
||||
} = useDraggable({
|
||||
id: header.id,
|
||||
// This ensures the drag overlay matches the element's position exactly
|
||||
data: {
|
||||
type: 'header'
|
||||
}
|
||||
});
|
||||
|
||||
// Set up droppable area to detect when a header is over this target
|
||||
const { setNodeRef: setDropNodeRef } = useDroppable({
|
||||
id: `droppable-${header.id}`
|
||||
});
|
||||
|
||||
const isOverTarget = overTargetId === header.id;
|
||||
|
||||
const style: CSSProperties = {
|
||||
position: 'relative',
|
||||
whiteSpace: 'nowrap',
|
||||
width: header.column.getSize(),
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
transition: 'none', // Prevent any transitions for snappy changes
|
||||
height: `${HEADER_HEIGHT}px` // Set fixed header height
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
ref={setDropNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
'group relative border-r select-none last:border-r-0',
|
||||
header.column.getIsResizing() && 'bg-primary/8',
|
||||
isOverTarget &&
|
||||
'bg-primary/10 border-primary inset rounded-sm border border-r border-dashed'
|
||||
)}
|
||||
// onClick toggles sorting if enabled
|
||||
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}>
|
||||
<span
|
||||
className="flex h-full flex-1 items-center space-x-1.5 p-2"
|
||||
ref={sortable ? setDragNodeRef : undefined}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
style={{ cursor: 'grab' }}>
|
||||
<span className="text-gray-dark text-base font-normal">
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</span>
|
||||
{header.column.getIsSorted() === 'asc' && (
|
||||
<span className="text-icon-color text-xs">
|
||||
<CaretUp />
|
||||
</span>
|
||||
)}
|
||||
{header.column.getIsSorted() === 'desc' && (
|
||||
<span className="text-icon-color text-xs">
|
||||
<CaretDown />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{resizable && (
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}>
|
||||
<span
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={cn(
|
||||
'hover:bg-primary group-hover:bg-border absolute top-0 -right-[3px] z-10 h-full w-1 cursor-col-resize transition-colors duration-100 select-none hover:w-1',
|
||||
header.column.getIsResizing() && 'bg-primary'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DraggableHeader.displayName = 'DraggableHeader';
|
||||
|
||||
// Header content component to use in the DragOverlay
|
||||
const HeaderDragOverlay = ({
|
||||
header
|
||||
}: {
|
||||
header: Header<Record<string, string | number | Date | null>, unknown>;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center rounded-sm border bg-white p-2 shadow-lg"
|
||||
style={{
|
||||
width: header.column.getSize(),
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
opacity: 0.85,
|
||||
transform: 'translate3d(0, 0, 0)', // Ensure no unexpected transforms are applied
|
||||
pointerEvents: 'none' // Prevent the overlay from intercepting pointer events
|
||||
}}>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{header.column.getIsSorted() === 'asc' && <span> 🔼</span>}
|
||||
{header.column.getIsSorted() === 'desc' && <span> 🔽</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface DataGridHeaderProps {
|
||||
table: Table<Record<string, string | number | Date | null>>;
|
||||
sortable: boolean;
|
||||
resizable: boolean;
|
||||
colOrder: string[];
|
||||
setColOrder: (colOrder: string[]) => void;
|
||||
onReorderColumns?: (colOrder: string[]) => void;
|
||||
}
|
||||
|
||||
export const DataGridHeader: React.FC<DataGridHeaderProps> = ({
|
||||
table,
|
||||
colOrder,
|
||||
sortable,
|
||||
resizable,
|
||||
setColOrder,
|
||||
onReorderColumns
|
||||
}) => {
|
||||
// Track active drag item and over target
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [overTargetId, setOverTargetId] = useState<string | null>(null);
|
||||
// Store active header for overlay rendering
|
||||
const [activeHeader, setActiveHeader] = useState<Header<
|
||||
Record<string, string | number | Date | null>,
|
||||
unknown
|
||||
> | null>(null);
|
||||
|
||||
// Reference to the style element for cursor handling
|
||||
const styleRef = useRef<HTMLStyleElement | null>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
distance: 2
|
||||
}
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
distance: 2
|
||||
}
|
||||
}),
|
||||
useSensor(KeyboardSensor)
|
||||
);
|
||||
|
||||
// Handle drag start to capture the active header
|
||||
const onDragStart = useMemoizedFn((event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
setActiveId(active.id as string);
|
||||
|
||||
// Add global cursor style
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `* { cursor: grabbing !important; }`;
|
||||
document.head.appendChild(style);
|
||||
styleRef.current = style;
|
||||
|
||||
// Find and store the active header for the overlay
|
||||
const headerIndex = table
|
||||
.getHeaderGroups()[0]
|
||||
?.headers.findIndex((header) => header.id === active.id);
|
||||
|
||||
if (headerIndex !== undefined && headerIndex !== -1) {
|
||||
setActiveHeader(table.getHeaderGroups()[0]?.headers[headerIndex]);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle drag over to highlight the target
|
||||
const onDragOver = useMemoizedFn((event: DragOverEvent) => {
|
||||
const { over } = event;
|
||||
if (over) {
|
||||
// Extract the actual header ID from the droppable ID
|
||||
const headerId = over.id.toString().replace('droppable-', '');
|
||||
setOverTargetId(headerId);
|
||||
} else {
|
||||
setOverTargetId(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle drag end to reorder columns.
|
||||
const onDragEnd = useMemoizedFn((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
// Remove global cursor style
|
||||
if (styleRef.current) {
|
||||
document.head.removeChild(styleRef.current);
|
||||
styleRef.current = null;
|
||||
}
|
||||
|
||||
// Reset states immediately to prevent animation
|
||||
setActiveId(null);
|
||||
setActiveHeader(null);
|
||||
setOverTargetId(null);
|
||||
|
||||
if (active && over) {
|
||||
// Extract the actual header ID from the droppable ID
|
||||
const overId = over.id.toString().replace('droppable-', '');
|
||||
|
||||
if (active.id !== overId) {
|
||||
const oldIndex = colOrder.indexOf(active.id as string);
|
||||
const newIndex = colOrder.indexOf(overId);
|
||||
const newOrder = arrayMove(colOrder, oldIndex, newIndex);
|
||||
setColOrder(newOrder);
|
||||
if (onReorderColumns) onReorderColumns(newOrder);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up any styles on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clean up cursor style if component unmounts during a drag
|
||||
if (styleRef.current) {
|
||||
document.head.removeChild(styleRef.current);
|
||||
styleRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<thead className="bg-background sticky top-0 z-10 w-full" suppressHydrationWarning>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
modifiers={[restrictToHorizontalAxis]}
|
||||
collisionDetection={pointerWithin}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}>
|
||||
<tr className="flex border-b">
|
||||
{table
|
||||
.getHeaderGroups()[0]
|
||||
?.headers.map(
|
||||
(header: Header<Record<string, string | number | Date | null>, unknown>) => (
|
||||
<DraggableHeader
|
||||
key={header.id}
|
||||
header={header}
|
||||
sortable={sortable}
|
||||
resizable={resizable}
|
||||
overTargetId={overTargetId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
|
||||
{/* Drag Overlay */}
|
||||
<DragOverlay
|
||||
wrapperElement="span"
|
||||
adjustScale={false}
|
||||
dropAnimation={null} // Using null to completely disable animation
|
||||
zIndex={1000}>
|
||||
{activeId && activeHeader && <HeaderDragOverlay header={activeHeader} />}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
|
||||
DataGridHeader.displayName = 'DataGridHeader';
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { Row } from '@tanstack/react-table';
|
||||
import { VirtualItem } from '@tanstack/react-virtual';
|
||||
import { DataGridCell } from './DataGridCell';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
interface DataGridRowProps {
|
||||
row: Row<Record<string, string | number | Date | null>>;
|
||||
virtualRow: VirtualItem;
|
||||
}
|
||||
|
||||
export const DataGridRow: React.FC<DataGridRowProps> = ({ row, virtualRow }) => {
|
||||
return (
|
||||
<tr
|
||||
className={cn(
|
||||
'hover:bg-item-hover absolute inset-x-0 flex border-b last:border-b-0',
|
||||
row.getIsSelected() && 'bg-primary/10'
|
||||
)}
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
height: `${virtualRow.size}px`
|
||||
}}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<DataGridCell key={cell.id} cell={cell} />
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
DataGridRow.displayName = 'DataGridRow';
|
|
@ -1,36 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import React, { useMemo, useRef, useState, useEffect, CSSProperties } from 'react';
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
SortingState,
|
||||
Header
|
||||
SortingState
|
||||
} from '@tanstack/react-table';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import {
|
||||
DndContext,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
KeyboardSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
pointerWithin,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
useDraggable,
|
||||
useDroppable,
|
||||
DragOverEvent
|
||||
} from '@dnd-kit/core';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import sampleSize from 'lodash/sampleSize';
|
||||
import { defaultCellFormat, defaultHeaderFormat } from '../helpers';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { CaretDown, CaretUp } from '../../../icons/NucleoIconFilled';
|
||||
import { DataGridHeader } from './DataGridHeader';
|
||||
import { DataGridRow } from './DataGridRow';
|
||||
import { CELL_HEIGHT, OVERSCAN } from './constants';
|
||||
import { initializeColumnWidths } from './initializeColumnWidths';
|
||||
|
||||
export interface TanStackDataGridProps {
|
||||
className?: string;
|
||||
|
@ -51,125 +35,6 @@ export interface TanStackDataGridProps {
|
|||
) => void;
|
||||
}
|
||||
|
||||
interface DraggableHeaderProps {
|
||||
header: Header<Record<string, string | number | Date | null>, unknown>;
|
||||
resizable: boolean;
|
||||
sortable: boolean;
|
||||
overTargetId: string | null;
|
||||
}
|
||||
|
||||
// Constants for consistent sizing
|
||||
const HEADER_HEIGHT = 36; // 9*4 = 36px (h-9 in Tailwind)
|
||||
|
||||
const DraggableHeader: React.FC<DraggableHeaderProps> = React.memo(
|
||||
({ header, sortable, resizable, overTargetId }) => {
|
||||
// Set up dnd-kit's useDraggable for this header cell
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
isDragging,
|
||||
setNodeRef: setDragNodeRef
|
||||
} = useDraggable({
|
||||
id: header.id,
|
||||
// This ensures the drag overlay matches the element's position exactly
|
||||
data: {
|
||||
type: 'header'
|
||||
}
|
||||
});
|
||||
|
||||
// Set up droppable area to detect when a header is over this target
|
||||
const { setNodeRef: setDropNodeRef } = useDroppable({
|
||||
id: `droppable-${header.id}`
|
||||
});
|
||||
|
||||
const isOverTarget = overTargetId === header.id;
|
||||
|
||||
const style: CSSProperties = {
|
||||
position: 'relative',
|
||||
whiteSpace: 'nowrap',
|
||||
width: header.column.getSize(),
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
transition: 'none', // Prevent any transitions for snappy changes
|
||||
height: `${HEADER_HEIGHT}px` // Set fixed header height
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
ref={setDropNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
'relative border-r select-none last:border-r-0',
|
||||
isOverTarget &&
|
||||
'bg-primary/10 border-primary inset rounded-sm border border-r border-dashed'
|
||||
)}
|
||||
// onClick toggles sorting if enabled
|
||||
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}>
|
||||
<div
|
||||
className="flex h-full flex-1 items-center space-x-1.5 p-2"
|
||||
ref={sortable ? setDragNodeRef : undefined}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
style={{ cursor: 'grab' }}>
|
||||
<span className="text-gray-dark text-base font-normal">
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</span>
|
||||
{header.column.getIsSorted() === 'asc' && (
|
||||
<span className="text-icon-color text-xs">
|
||||
<CaretUp />
|
||||
</span>
|
||||
)}
|
||||
{header.column.getIsSorted() === 'desc' && (
|
||||
<span className="text-icon-color text-xs">
|
||||
<CaretDown />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{resizable && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}>
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={cn(
|
||||
'hover:bg-primary absolute top-0 -right-[3px] z-10 h-full w-1 cursor-col-resize rounded transition-colors duration-200 select-none',
|
||||
header.column.getIsResizing() && 'bg-primary'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DraggableHeader.displayName = 'DraggableHeader';
|
||||
|
||||
// Header content component to use in the DragOverlay
|
||||
const HeaderDragOverlay = ({
|
||||
header
|
||||
}: {
|
||||
header: Header<Record<string, string | number | Date | null>, unknown>;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center rounded border bg-white p-2 shadow-lg"
|
||||
style={{
|
||||
width: header.column.getSize(),
|
||||
height: `${HEADER_HEIGHT}px`,
|
||||
opacity: 0.85,
|
||||
transform: 'translate3d(0, 0, 0)', // Ensure no unexpected transforms are applied
|
||||
pointerEvents: 'none' // Prevent the overlay from intercepting pointer events
|
||||
}}>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{header.column.getIsSorted() === 'asc' && <span> 🔼</span>}
|
||||
{header.column.getIsSorted() === 'desc' && <span> 🔽</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TanStackDataGrid: React.FC<TanStackDataGridProps> = React.memo(
|
||||
({
|
||||
className = '',
|
||||
|
@ -189,30 +54,13 @@ export const TanStackDataGrid: React.FC<TanStackDataGridProps> = React.memo(
|
|||
return Object.keys(rows[0] || {});
|
||||
}, [rows]);
|
||||
|
||||
// (Optional) Use a sample of rows for preview purposes.
|
||||
const sampleOfRows = useMemo(() => sampleSize(rows, 15), [rows]);
|
||||
|
||||
// Set up initial states for sorting, column sizing, and column order.
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnSizing, setColumnSizing] = useState(() => {
|
||||
const initial: Record<string, number> = {};
|
||||
fields.forEach((field) => {
|
||||
initial[field] = columnWidthsProp?.[field] || 100;
|
||||
});
|
||||
return initial;
|
||||
return initializeColumnWidths(fields, rows, columnWidthsProp, cellFormat, headerFormat);
|
||||
});
|
||||
const [colOrder, setColOrder] = useState<string[]>(serverColumnOrder || fields);
|
||||
|
||||
// Track active drag item and over target
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [overTargetId, setOverTargetId] = useState<string | null>(null);
|
||||
|
||||
// Store active header for overlay rendering
|
||||
const [activeHeader, setActiveHeader] = useState<Header<
|
||||
Record<string, string | number | Date | null>,
|
||||
unknown
|
||||
> | null>(null);
|
||||
|
||||
// Build columns from fields.
|
||||
const columns = useMemo<
|
||||
ColumnDef<Record<string, string | number | Date | null>, string | number | Date | null>[]
|
||||
|
@ -246,6 +94,15 @@ export const TanStackDataGrid: React.FC<TanStackDataGridProps> = React.memo(
|
|||
columnResizeMode: 'onChange'
|
||||
});
|
||||
|
||||
// Set up the virtualizer for infinite scrolling.
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: table.getRowModel().rows.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => CELL_HEIGHT, // estimated row height
|
||||
overscan: OVERSCAN
|
||||
});
|
||||
|
||||
// Notify when column sizing changes.
|
||||
useEffect(() => {
|
||||
if (onResizeColumns) {
|
||||
|
@ -259,140 +116,17 @@ export const TanStackDataGrid: React.FC<TanStackDataGridProps> = React.memo(
|
|||
if (onReady) onReady();
|
||||
}, [onReady]);
|
||||
|
||||
// Set up dnd-kit sensors.
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
distance: 2
|
||||
}
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
distance: 2
|
||||
}
|
||||
}),
|
||||
useSensor(KeyboardSensor)
|
||||
);
|
||||
|
||||
// Reference to the style element for cursor handling
|
||||
const styleRef = useRef<HTMLStyleElement | null>(null);
|
||||
|
||||
// Handle drag start to capture the active header
|
||||
const handleDragStart = useMemoizedFn((event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
setActiveId(active.id as string);
|
||||
|
||||
// Add global cursor style
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `* { cursor: grabbing !important; }`;
|
||||
document.head.appendChild(style);
|
||||
styleRef.current = style;
|
||||
|
||||
// Find and store the active header for the overlay
|
||||
const headerIndex = table
|
||||
.getHeaderGroups()[0]
|
||||
?.headers.findIndex((header) => header.id === active.id);
|
||||
|
||||
if (headerIndex !== undefined && headerIndex !== -1) {
|
||||
setActiveHeader(table.getHeaderGroups()[0]?.headers[headerIndex]);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle drag over to highlight the target
|
||||
const handleDragOver = useMemoizedFn((event: DragOverEvent) => {
|
||||
const { over } = event;
|
||||
if (over) {
|
||||
// Extract the actual header ID from the droppable ID
|
||||
const headerId = over.id.toString().replace('droppable-', '');
|
||||
setOverTargetId(headerId);
|
||||
} else {
|
||||
setOverTargetId(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle drag end to reorder columns.
|
||||
const handleDragEnd = useMemoizedFn((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
// Remove global cursor style
|
||||
if (styleRef.current) {
|
||||
document.head.removeChild(styleRef.current);
|
||||
styleRef.current = null;
|
||||
}
|
||||
|
||||
// Reset states immediately to prevent animation
|
||||
setActiveId(null);
|
||||
setActiveHeader(null);
|
||||
setOverTargetId(null);
|
||||
|
||||
if (active && over) {
|
||||
// Extract the actual header ID from the droppable ID
|
||||
const overId = over.id.toString().replace('droppable-', '');
|
||||
|
||||
if (active.id !== overId) {
|
||||
const oldIndex = colOrder.indexOf(active.id as string);
|
||||
const newIndex = colOrder.indexOf(overId);
|
||||
const newOrder = arrayMove(colOrder, oldIndex, newIndex);
|
||||
setColOrder(newOrder);
|
||||
if (onReorderColumns) onReorderColumns(newOrder);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up any styles on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clean up cursor style if component unmounts during a drag
|
||||
if (styleRef.current) {
|
||||
document.head.removeChild(styleRef.current);
|
||||
styleRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Set up the virtualizer for infinite scrolling.
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: table.getRowModel().rows.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 36, // estimated row height
|
||||
overscan: 5
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className={cn('h-full w-full overflow-auto', className)}>
|
||||
<div ref={parentRef} className={cn('h-full w-full overflow-auto border', className)}>
|
||||
<table className="bg-background w-full">
|
||||
{/* Header */}
|
||||
<thead className="bg-background sticky top-0 z-10 w-full">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
modifiers={[restrictToHorizontalAxis]}
|
||||
collisionDetection={pointerWithin}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}>
|
||||
<tr className="flex border-b">
|
||||
{table
|
||||
.getHeaderGroups()[0]
|
||||
?.headers.map((header) => (
|
||||
<DraggableHeader
|
||||
header={header}
|
||||
sortable={sortable}
|
||||
resizable={resizable}
|
||||
overTargetId={overTargetId}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
{/* Drag Overlay */}
|
||||
<DragOverlay
|
||||
adjustScale={false}
|
||||
dropAnimation={null} // Using null to completely disable animation
|
||||
zIndex={1000}>
|
||||
{activeId && activeHeader && <HeaderDragOverlay header={activeHeader} />}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</thead>
|
||||
<DataGridHeader
|
||||
table={table}
|
||||
sortable={sortable}
|
||||
resizable={resizable}
|
||||
colOrder={colOrder}
|
||||
setColOrder={setColOrder}
|
||||
onReorderColumns={onReorderColumns}
|
||||
/>
|
||||
|
||||
{/* Body */}
|
||||
<tbody
|
||||
|
@ -400,24 +134,7 @@ export const TanStackDataGrid: React.FC<TanStackDataGridProps> = React.memo(
|
|||
style={{ display: 'grid', height: `${rowVirtualizer.getTotalSize()}px` }}>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const row = table.getRowModel().rows[virtualRow.index];
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className="absolute inset-x-0 flex border-b last:border-b-0"
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
height: `${virtualRow.size}px`
|
||||
}}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className="border-r p-2 last:border-r-0"
|
||||
style={{ width: cell.column.getSize() }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
return <DataGridRow key={row.id} row={row} virtualRow={virtualRow} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// Constants for consistent sizing
|
||||
export const HEADER_HEIGHT = 28; // 9*4 = 36px (h-9 in Tailwind)
|
||||
export const CELL_HEIGHT = 28; // 9*4 = 36px (h-9 in Tailwind)
|
||||
export const OVERSCAN = 5;
|
||||
|
||||
export const MIN_COLUMN_WIDTH = 80;
|
||||
export const MAX_COLUMN_WIDTH = 350;
|
|
@ -0,0 +1 @@
|
|||
export * from './TanStackDataGrid';
|
|
@ -0,0 +1,38 @@
|
|||
import { measureTextWidth } from '@/lib';
|
||||
import sampleSize from 'lodash/sampleSize';
|
||||
import clamp from 'lodash/clamp';
|
||||
import { MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH } from './constants';
|
||||
|
||||
export const initializeColumnWidths = (
|
||||
fields: string[],
|
||||
rows: Record<string, string | number | null | Date>[],
|
||||
columnWidthsProp: Record<string, number> | undefined,
|
||||
cellFormat: (value: string | number | null | Date, field: string) => string,
|
||||
headerFormat: (value: string | number | Date | null, columnName: string) => string
|
||||
) => {
|
||||
const sampleOfRows = sampleSize(rows, 15);
|
||||
const initial: Record<string, number> = {};
|
||||
fields.forEach((field) => {
|
||||
initial[field] =
|
||||
columnWidthsProp?.[field] ||
|
||||
getDefaultColumnWidth(sampleOfRows, field, cellFormat, headerFormat);
|
||||
});
|
||||
return initial;
|
||||
};
|
||||
|
||||
const getDefaultColumnWidth = (
|
||||
rows: Record<string, string | number | null | Date>[],
|
||||
field: string,
|
||||
cellFormat: (value: string | number | null | Date, field: string) => string,
|
||||
headerFormat: (value: string | number | Date | null, columnName: string) => string
|
||||
) => {
|
||||
const headerString = headerFormat(field, field);
|
||||
const longestString = rows.reduce((acc, curr) => {
|
||||
const currString = cellFormat(curr[field], field);
|
||||
if (!currString) return acc;
|
||||
return acc.length > currString.length ? acc : currString;
|
||||
}, headerString);
|
||||
const longestWidth = measureTextWidth(longestString).width + 10;
|
||||
const width = clamp(longestWidth, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
|
||||
return width;
|
||||
};
|
Loading…
Reference in New Issue