mirror of https://github.com/buster-so/buster.git
Update AppDataGrid2.tsx
This commit is contained in:
parent
39d7d575cc
commit
8205e72f3f
|
@ -62,7 +62,7 @@ export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
rows: sampleData,
|
rows: sampleData,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
draggable: true,
|
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
|
@ -76,7 +76,7 @@ export const NonResizable: Story = {
|
||||||
args: {
|
args: {
|
||||||
rows: sampleData,
|
rows: sampleData,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
draggable: true,
|
|
||||||
sortable: true
|
sortable: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -85,8 +85,7 @@ export const NonDraggable: Story = {
|
||||||
args: {
|
args: {
|
||||||
rows: sampleData,
|
rows: sampleData,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
draggable: false,
|
sortable: false
|
||||||
sortable: true
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -94,7 +93,7 @@ export const NonSortable: Story = {
|
||||||
args: {
|
args: {
|
||||||
rows: sampleData,
|
rows: sampleData,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
draggable: true,
|
|
||||||
sortable: false
|
sortable: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -104,7 +103,7 @@ export const CustomColumnOrder: Story = {
|
||||||
rows: sampleData,
|
rows: sampleData,
|
||||||
columnOrder: ['name', 'email', 'age', 'id', 'joinDate'],
|
columnOrder: ['name', 'email', 'age', 'id', 'joinDate'],
|
||||||
resizable: true,
|
resizable: true,
|
||||||
draggable: true,
|
|
||||||
sortable: true
|
sortable: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -120,7 +119,7 @@ export const CustomColumnWidths: Story = {
|
||||||
joinDate: 120
|
joinDate: 120
|
||||||
},
|
},
|
||||||
resizable: true,
|
resizable: true,
|
||||||
draggable: true,
|
|
||||||
sortable: true
|
sortable: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -139,7 +138,7 @@ export const CustomFormatting: Story = {
|
||||||
return String(value);
|
return String(value);
|
||||||
},
|
},
|
||||||
resizable: true,
|
resizable: true,
|
||||||
draggable: true,
|
|
||||||
sortable: true
|
sortable: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -151,7 +150,7 @@ export const WithCallbacks: Story = {
|
||||||
onResizeColumns: (columnSizes) => console.log('Columns resized:', columnSizes),
|
onResizeColumns: (columnSizes) => console.log('Columns resized:', columnSizes),
|
||||||
onReady: () => console.log('Grid is ready'),
|
onReady: () => console.log('Grid is ready'),
|
||||||
resizable: true,
|
resizable: true,
|
||||||
draggable: true,
|
|
||||||
sortable: true
|
sortable: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -170,7 +169,7 @@ export const ManyRows: Story = {
|
||||||
).toISOString()
|
).toISOString()
|
||||||
})),
|
})),
|
||||||
resizable: true,
|
resizable: true,
|
||||||
draggable: true,
|
|
||||||
sortable: true
|
sortable: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,31 +17,31 @@ import {
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
|
pointerWithin,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
DragStartEvent,
|
DragStartEvent,
|
||||||
useDraggable,
|
useDraggable,
|
||||||
useDroppable,
|
useDroppable,
|
||||||
DragOverEvent,
|
DragOverEvent
|
||||||
defaultDropAnimationSideEffects
|
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { arrayMove } from '@dnd-kit/sortable';
|
import { arrayMove } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import sampleSize from 'lodash/sampleSize';
|
import sampleSize from 'lodash/sampleSize';
|
||||||
import { defaultCellFormat, defaultHeaderFormat } from './helpers';
|
import { defaultCellFormat, defaultHeaderFormat } from './helpers';
|
||||||
import { cn } from '@/lib/classMerge';
|
import { cn } from '@/lib/classMerge';
|
||||||
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
|
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
|
||||||
|
import { useMemoizedFn } from '@/hooks';
|
||||||
|
import { CaretDown, CaretUp } from '../../icons/NucleoIconFilled';
|
||||||
|
|
||||||
export interface AppDataGridProps {
|
export interface AppDataGridProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
resizable?: boolean;
|
resizable?: boolean;
|
||||||
draggable?: boolean;
|
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
rows: Record<string, string | number | null | Date>[];
|
rows: Record<string, string | number | null | Date>[];
|
||||||
columnOrder?: string[];
|
columnOrder?: string[];
|
||||||
columnWidths?: Record<string, number>;
|
columnWidths?: Record<string, number>;
|
||||||
headerFormat?: (value: any, columnName: string) => string;
|
headerFormat?: (value: string | number | Date | null, columnName: string) => string;
|
||||||
cellFormat?: (value: any, columnName: string) => string;
|
cellFormat?: (value: string | number | Date | null, columnName: string) => string;
|
||||||
onReorderColumns?: (columnIds: string[]) => void;
|
onReorderColumns?: (columnIds: string[]) => void;
|
||||||
onReady?: () => void;
|
onReady?: () => void;
|
||||||
onResizeColumns?: (
|
onResizeColumns?: (
|
||||||
|
@ -53,95 +53,113 @@ export interface AppDataGridProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DraggableHeaderProps {
|
interface DraggableHeaderProps {
|
||||||
header: any; // Header<any, unknown>
|
header: Header<Record<string, string | number | Date | null>, unknown>;
|
||||||
sortable: boolean;
|
|
||||||
resizable: boolean;
|
resizable: boolean;
|
||||||
isOverTarget: boolean;
|
sortable: boolean;
|
||||||
|
overTargetId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants for consistent sizing
|
// Constants for consistent sizing
|
||||||
const HEADER_HEIGHT = 36; // 9*4 = 36px (h-9 in Tailwind)
|
const HEADER_HEIGHT = 36; // 9*4 = 36px (h-9 in Tailwind)
|
||||||
|
|
||||||
const DraggableHeader: React.FC<DraggableHeaderProps> = ({
|
const DraggableHeader: React.FC<DraggableHeaderProps> = React.memo(
|
||||||
header,
|
({ header, sortable, resizable, overTargetId }) => {
|
||||||
sortable,
|
// Set up dnd-kit's useDraggable for this header cell
|
||||||
resizable,
|
const {
|
||||||
isOverTarget
|
attributes,
|
||||||
}) => {
|
listeners,
|
||||||
// Set up dnd-kit's useDraggable for this header cell
|
isDragging,
|
||||||
const {
|
setNodeRef: setDragNodeRef
|
||||||
attributes,
|
} = useDraggable({
|
||||||
listeners,
|
id: header.id,
|
||||||
isDragging,
|
// This ensures the drag overlay matches the element's position exactly
|
||||||
setNodeRef: setDragNodeRef
|
data: {
|
||||||
} = useDraggable({
|
type: 'header'
|
||||||
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
|
// Set up droppable area to detect when a header is over this target
|
||||||
const { setNodeRef: setDropNodeRef } = useDroppable({
|
const { setNodeRef: setDropNodeRef } = useDroppable({
|
||||||
id: `droppable-${header.id}`
|
id: `droppable-${header.id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const style: CSSProperties = {
|
const isOverTarget = overTargetId === header.id;
|
||||||
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 (
|
const style: CSSProperties = {
|
||||||
<div
|
position: 'relative',
|
||||||
ref={setDropNodeRef}
|
whiteSpace: 'nowrap',
|
||||||
style={style}
|
width: header.column.getSize(),
|
||||||
className={cn(
|
opacity: isDragging ? 0.4 : 1,
|
||||||
'bg-background relative border select-none',
|
transition: 'none', // Prevent any transitions for snappy changes
|
||||||
isOverTarget && 'bg-primary/10 border-primary rounded-sm border-dashed'
|
height: `${HEADER_HEIGHT}px` // Set fixed header height
|
||||||
)}
|
};
|
||||||
// onClick toggles sorting if enabled
|
|
||||||
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}>
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex h-full flex-1 items-center p-2"
|
ref={setDropNodeRef}
|
||||||
ref={setDragNodeRef}
|
style={style}
|
||||||
{...attributes}
|
className={cn(
|
||||||
{...listeners}
|
'bg-background relative border select-none',
|
||||||
style={{ cursor: 'grab' }}>
|
isOverTarget && 'bg-primary/10 border-primary rounded-sm border-dashed'
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
)}
|
||||||
{header.column.getIsSorted() === 'asc' && <span> 🔼</span>}
|
// onClick toggles sorting if enabled
|
||||||
{header.column.getIsSorted() === 'desc' && <span> 🔽</span>}
|
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}>
|
||||||
</div>
|
|
||||||
{resizable && (
|
|
||||||
<div
|
<div
|
||||||
onClick={(e) => {
|
className="flex h-full flex-1 items-center space-x-1 p-2"
|
||||||
e.stopPropagation();
|
ref={sortable ? setDragNodeRef : undefined}
|
||||||
e.preventDefault();
|
{...attributes}
|
||||||
}}>
|
{...listeners}
|
||||||
<div
|
style={{ cursor: 'grab' }}>
|
||||||
onMouseDown={header.getResizeHandler()}
|
<span className="text-gray-dark">
|
||||||
onTouchStart={header.getResizeHandler()}
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
className="absolute top-0 right-0 h-full w-2 cursor-col-resize select-none"
|
</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>
|
</div>
|
||||||
)}
|
{resizable && (
|
||||||
</div>
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
DraggableHeader.displayName = 'DraggableHeader';
|
||||||
|
|
||||||
// Header content component to use in the DragOverlay
|
// Header content component to use in the DragOverlay
|
||||||
const HeaderDragOverlay = ({ header }: { header: Header<any, unknown> }) => {
|
const HeaderDragOverlay = ({
|
||||||
|
header
|
||||||
|
}: {
|
||||||
|
header: Header<Record<string, string | number | Date | null>, unknown>;
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center rounded border bg-white p-2 shadow-lg"
|
className="flex items-center rounded border bg-white p-2 shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
width: header.column.getSize(),
|
width: header.column.getSize(),
|
||||||
height: `${HEADER_HEIGHT}px`,
|
height: `${HEADER_HEIGHT}px`,
|
||||||
opacity: 0.9,
|
opacity: 0.85,
|
||||||
transform: 'translate3d(0, 0, 0)', // Ensure no unexpected transforms are applied
|
transform: 'translate3d(0, 0, 0)', // Ensure no unexpected transforms are applied
|
||||||
pointerEvents: 'none' // Prevent the overlay from intercepting pointer events
|
pointerEvents: 'none' // Prevent the overlay from intercepting pointer events
|
||||||
}}>
|
}}>
|
||||||
|
@ -156,7 +174,6 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
({
|
({
|
||||||
className = '',
|
className = '',
|
||||||
resizable = true,
|
resizable = true,
|
||||||
draggable = true,
|
|
||||||
sortable = true,
|
sortable = true,
|
||||||
columnWidths: columnWidthsProp,
|
columnWidths: columnWidthsProp,
|
||||||
columnOrder: serverColumnOrder,
|
columnOrder: serverColumnOrder,
|
||||||
|
@ -191,10 +208,15 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
const [overTargetId, setOverTargetId] = useState<string | null>(null);
|
const [overTargetId, setOverTargetId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Store active header for overlay rendering
|
// Store active header for overlay rendering
|
||||||
const [activeHeader, setActiveHeader] = useState<any>(null);
|
const [activeHeader, setActiveHeader] = useState<Header<
|
||||||
|
Record<string, string | number | Date | null>,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
// Build columns from fields.
|
// Build columns from fields.
|
||||||
const columns = useMemo<ColumnDef<any, any>[]>(
|
const columns = useMemo<
|
||||||
|
ColumnDef<Record<string, string | number | Date | null>, string | number | Date | null>[]
|
||||||
|
>(
|
||||||
() =>
|
() =>
|
||||||
fields.map((field) => ({
|
fields.map((field) => ({
|
||||||
id: field,
|
id: field,
|
||||||
|
@ -252,11 +274,20 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
useSensor(KeyboardSensor)
|
useSensor(KeyboardSensor)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reference to the style element for cursor handling
|
||||||
|
const styleRef = useRef<HTMLStyleElement | null>(null);
|
||||||
|
|
||||||
// Handle drag start to capture the active header
|
// Handle drag start to capture the active header
|
||||||
const handleDragStart = (event: DragStartEvent) => {
|
const handleDragStart = useMemoizedFn((event: DragStartEvent) => {
|
||||||
const { active } = event;
|
const { active } = event;
|
||||||
setActiveId(active.id as string);
|
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
|
// Find and store the active header for the overlay
|
||||||
const headerIndex = table
|
const headerIndex = table
|
||||||
.getHeaderGroups()[0]
|
.getHeaderGroups()[0]
|
||||||
|
@ -265,10 +296,10 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
if (headerIndex !== undefined && headerIndex !== -1) {
|
if (headerIndex !== undefined && headerIndex !== -1) {
|
||||||
setActiveHeader(table.getHeaderGroups()[0]?.headers[headerIndex]);
|
setActiveHeader(table.getHeaderGroups()[0]?.headers[headerIndex]);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Handle drag over to highlight the target
|
// Handle drag over to highlight the target
|
||||||
const handleDragOver = (event: DragOverEvent) => {
|
const handleDragOver = useMemoizedFn((event: DragOverEvent) => {
|
||||||
const { over } = event;
|
const { over } = event;
|
||||||
if (over) {
|
if (over) {
|
||||||
// Extract the actual header ID from the droppable ID
|
// Extract the actual header ID from the droppable ID
|
||||||
|
@ -277,12 +308,18 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
} else {
|
} else {
|
||||||
setOverTargetId(null);
|
setOverTargetId(null);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Handle drag end to reorder columns.
|
// Handle drag end to reorder columns.
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
const handleDragEnd = useMemoizedFn((event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
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
|
// Reset states immediately to prevent animation
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
setActiveHeader(null);
|
setActiveHeader(null);
|
||||||
|
@ -300,7 +337,18 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
if (onReorderColumns) onReorderColumns(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.
|
// Set up the virtualizer for infinite scrolling.
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -332,7 +380,7 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
modifiers={[restrictToHorizontalAxis]}
|
modifiers={[restrictToHorizontalAxis]}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={pointerWithin}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragEnd={handleDragEnd}>
|
onDragEnd={handleDragEnd}>
|
||||||
|
@ -345,7 +393,7 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
header={header}
|
header={header}
|
||||||
sortable={sortable}
|
sortable={sortable}
|
||||||
resizable={resizable}
|
resizable={resizable}
|
||||||
isOverTarget={header.id === overTargetId}
|
overTargetId={overTargetId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -372,12 +420,9 @@ export const AppDataGrid2: React.FC<AppDataGridProps> = React.memo(
|
||||||
height: `${virtualRow.size}px`
|
height: `${virtualRow.size}px`
|
||||||
}}>
|
}}>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<div
|
<td key={cell.id} className="border p-2" style={{ width: cell.column.getSize() }}>
|
||||||
key={cell.id}
|
|
||||||
className="border p-2"
|
|
||||||
style={{ width: cell.column.getSize() }}>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</div>
|
</td>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue