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'; 'use client';
import { ProTable as _ProTable, ProTableProps } from '@repo/ui/pro-table'; import { ProTable as _ProTable, ProTableProps } from '@repo/ui/pro-table';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
export { type ProTableActions } from '@repo/ui/pro-table'; 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'); const t = useTranslations('common.table');
return ( return (
<_ProTable <_ProTable

View File

@ -15,6 +15,9 @@
"lint": "eslint . --max-warnings 0" "lint": "eslint . --max-warnings 0"
}, },
"dependencies": { "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", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@shadcn/ui": "workspace:*", "@shadcn/ui": "workspace:*",

View File

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

View File

@ -1,5 +1,20 @@
'use client'; '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 { Alert, AlertDescription, AlertTitle } from '@shadcn/ui/alert';
import { Button } from '@shadcn/ui/button'; import { Button } from '@shadcn/ui/button';
import { Checkbox } from '@shadcn/ui/checkbox'; import { Checkbox } from '@shadcn/ui/checkbox';
@ -17,13 +32,15 @@ import {
VisibilityState, VisibilityState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { useSize } from 'ahooks'; 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 React, { Fragment, useEffect, useImperativeHandle, useRef, useState } from 'react';
import Empty from '../empty'; import Empty from '../empty';
import { ColumnFilter, IParams } from './column-filter'; import { ColumnFilter, IParams } from './column-filter';
import { ColumnHeader } from './column-header'; import { ColumnHeader } from './column-header';
import { ColumnToggle } from './column-toggle'; import { ColumnToggle } from './column-toggle';
import { Pagination } from './pagination'; import { Pagination } from './pagination';
import { SortableRow } from './sortable-row';
export interface ProTableProps<TData, TValue> { export interface ProTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
request: ( request: (
@ -53,13 +70,18 @@ export interface ProTableProps<TData, TValue> {
selectedRowsText: (total: number) => string; selectedRowsText: (total: number) => string;
}>; }>;
empty?: React.ReactNode; empty?: React.ReactNode;
onSort?: (newOrder: TData[]) => void;
} }
export interface ProTableActions { export interface ProTableActions {
refresh: () => void; refresh: () => void;
reset: () => 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, columns,
request, request,
params, params,
@ -68,6 +90,7 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
action, action,
texts, texts,
empty, empty,
onSort,
}: ProTableProps<TData, TValue>) { }: ProTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
@ -84,6 +107,18 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
const table = useReactTable({ const table = useReactTable({
data, data,
columns: [ 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>()] : []), ...(actions?.batchRender ? [createSelectColumn<TData, TValue>()] : []),
...columns, ...columns,
...(actions?.render ...(actions?.render
@ -103,9 +138,10 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
}, },
] as ColumnDef<TData, TValue>[]) ] as ColumnDef<TData, TValue>[])
: []), : []),
], ] as ColumnDef<TData, TValue>[],
onPaginationChange: setPagination, onPaginationChange: setPagination,
onSortingChange: setSorting, onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
@ -123,6 +159,7 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
manualPagination: true, manualPagination: true,
manualFiltering: true, manualFiltering: true,
rowCount: rowCount, rowCount: rowCount,
manualSorting: true,
}); });
const fetchData = async () => { const fetchData = async () => {
@ -163,6 +200,32 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
fetchData(); fetchData();
}, [pagination.pageIndex, pagination.pageSize, columnFilters]); }, [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 selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original);
const selectedCount = selectedRows.length; const selectedCount = selectedRows.length;
@ -230,15 +293,50 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel()?.rows?.length ? ( {table.getRowModel()?.rows?.length ? (
table.getRowModel().rows.map((row) => ( onSort ? (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}> <DndContext
{row.getVisibleCells().map((cell) => ( sensors={sensors}
<TableCell key={cell.id} className={getTableCellClass(cell.column.id)}> collisionDetection={closestCenter}
{flexRender(cell.column.columnDef.cell, cell.getContext())} onDragEnd={handleDragEnd}
</TableCell> >
))} <SortableContext
</TableRow> 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> <TableRow>
<TableCell colSpan={columns.length + 2} className='py-24'> <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