✨ feat(table): Supports drag and drop sorting
This commit is contained in:
parent
0befdb0864
commit
2f56ef5eec
@ -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
|
||||
|
||||
@ -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:*",
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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'>
|
||||
|
||||
39
packages/ui/src/pro-table/sortable-row.tsx
Normal file
39
packages/ui/src/pro-table/sortable-row.tsx
Normal 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
10820
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user