Skip to content

Commit

Permalink
✨ feat(table): Supports drag and drop sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
web-ppanel committed Dec 19, 2024
1 parent 0befdb0 commit 2f56ef5
Show file tree
Hide file tree
Showing 6 changed files with 2,926 additions and 8,069 deletions.
10 changes: 6 additions & 4 deletions apps/admin/components/pro-table.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
1 change: 0 additions & 1 deletion packages/ui/src/pro-table/column-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
</>
Expand Down
122 changes: 110 additions & 12 deletions packages/ui/src/pro-table/pro-table.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: (
Expand Down Expand Up @@ -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,
Expand All @@ -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>([]);
Expand All @@ -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
Expand All @@ -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(),
Expand All @@ -123,6 +159,7 @@ export function ProTable<TData, TValue extends Record<string, unknown>>({
manualPagination: true,
manualFiltering: true,
rowCount: rowCount,
manualSorting: true,
});

const fetchData = async () => {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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'>
Expand Down
39 changes: 39 additions & 0 deletions packages/ui/src/pro-table/sortable-row.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
Loading

0 comments on commit 2f56ef5

Please sign in to comment.