feat(table): Supports drag and drop sorting

This commit is contained in:
web@ppanel 2024-12-19 16:58:10 +07:00
parent 0befdb0864
commit 2f56ef5eec
6 changed files with 2926 additions and 8069 deletions

View File

@ -1,11 +1,13 @@
'use client';
import { ProTable as _ProTable, ProTableProps } from '@repo/ui/pro-table';
import { useTranslations } from 'next-intl';
export { type ProTableActions } from '@repo/ui/pro-table';
export function ProTable<TData, TValue extends Record<string, unknown>>(
props: ProTableProps<TData, TValue>,
) {
export function ProTable<
TData extends Record<string, unknown>,
TValue extends Record<string, unknown>,
>(props: ProTableProps<TData, TValue>) {
const t = useTranslations('common.table');
return (
<_ProTable

View File

@ -15,6 +15,9 @@
"lint": "eslint . --max-warnings 0"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-icons": "^1.3.2",
"@shadcn/ui": "workspace:*",

View File

@ -64,7 +64,6 @@ export function ColumnHeader<TData, TValue>({
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className='text-muted-foreground/70 mr-2 h-3.5 w-3.5' />
Hide
{text?.hide || 'Hide'}
</DropdownMenuItem>
</>

View File

@ -1,5 +1,20 @@
'use client';
import {
closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { Alert, AlertDescription, AlertTitle } from '@shadcn/ui/alert';
import { Button } from '@shadcn/ui/button';
import { Checkbox } from '@shadcn/ui/checkbox';
@ -17,13 +32,15 @@ import {
VisibilityState,
} from '@tanstack/react-table';
import { useSize } from 'ahooks';
import { ListRestart, Loader, RefreshCcw } from 'lucide-react';
import { GripVertical, ListRestart, Loader, RefreshCcw } from 'lucide-react';
import React, { Fragment, useEffect, useImperativeHandle, useRef, useState } from 'react';
import Empty from '../empty';
import { ColumnFilter, IParams } from './column-filter';
import { ColumnHeader } from './column-header';
import { ColumnToggle } from './column-toggle';
import { Pagination } from './pagination';
import { SortableRow } from './sortable-row';
export interface ProTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
request: (
@ -53,13 +70,18 @@ export interface ProTableProps<TData, TValue> {
selectedRowsText: (total: number) => string;
}>;
empty?: React.ReactNode;
onSort?: (newOrder: TData[]) => void;
}
export interface ProTableActions {
refresh: () => void;
reset: () => void;
}
export function ProTable<TData, TValue extends Record<string, unknown>>({
export function ProTable<
TData extends Record<string, unknown> & { id?: string },
TValue extends Record<string, unknown>,
>({
columns,
request,
params,
@ -68,6 +90,7 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
action,
texts,
empty,
onSort,
}: ProTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
@ -84,6 +107,18 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
const table = useReactTable({
data,
columns: [
...(onSort
? [
{
id: 'sortable',
header: (
<GripVertical className='h-4 w-4 cursor-move text-gray-500 hover:text-gray-700' />
),
enableSorting: false,
enableHiding: false,
},
]
: []),
...(actions?.batchRender ? [createSelectColumn<TData, TValue>()] : []),
...columns,
...(actions?.render
@ -103,9 +138,10 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
},
] as ColumnDef<TData, TValue>[])
: []),
],
] as ColumnDef<TData, TValue>[],
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
@ -123,6 +159,7 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
manualPagination: true,
manualFiltering: true,
rowCount: rowCount,
manualSorting: true,
});
const fetchData = async () => {
@ -163,6 +200,32 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
fetchData();
}, [pagination.pageIndex, pagination.pageSize, columnFilters]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active?.id !== over?.id) {
setData((items) => {
const oldIndex = items.findIndex((item) => {
return String(item.id) === active?.id;
});
const newIndex = items.findIndex((item) => {
return String(item.id) === over?.id;
});
const newOrder = arrayMove(items, oldIndex, newIndex);
if (onSort) onSort(newOrder);
return newOrder;
});
}
};
const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original);
const selectedCount = selectedRows.length;
@ -230,15 +293,50 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
</TableHeader>
<TableBody>
{table.getRowModel()?.rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className={getTableCellClass(cell.column.id)}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
onSort ? (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={table.getRowModel()?.rows.map((row) => {
return String(row.original.id);
})}
strategy={verticalListSortingStrategy}
>
{table.getRowModel().rows.map((row) => (
<SortableRow
key={row.original.id ? String(row.original.id) : String(row.index)}
id={row.original.id ? String(row.original.id) : String(row.index)}
data-state={row.getIsSelected() && 'selected'}
isSortable
>
{row
.getVisibleCells()
.filter((cell) => {
return cell.column.id !== 'sortable';
})
.map((cell) => (
<TableCell key={cell.id} className={getTableCellClass(cell.column.id)}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</SortableRow>
))}
</SortableContext>
</DndContext>
) : (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className={getTableCellClass(cell.column.id)}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
)
) : (
<TableRow>
<TableCell colSpan={columns.length + 2} className='py-24'>

View File

@ -0,0 +1,39 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { TableCell, TableRow } from '@shadcn/ui/table';
import { GripVertical } from 'lucide-react';
import React from 'react';
interface SortableRowProps {
id: string;
children: React.ReactNode;
isSortable: boolean;
}
export function SortableRow({ id, children, isSortable }: SortableRowProps) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id,
disabled: !isSortable,
});
const style = {
transform: CSS.Transform.toString({
x: 0,
y: transform?.y || 0,
scaleX: transform?.scaleX || 1,
scaleY: transform?.scaleY || 1,
}),
transition,
};
return (
<TableRow ref={setNodeRef} style={style}>
{isSortable ? (
<TableCell className='cursor-move' {...listeners} {...attributes}>
<GripVertical className='h-4 w-4 cursor-move text-gray-500 hover:text-gray-700' />
</TableCell>
) : null}
{children}
</TableRow>
);
}

10820
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff