feat(table): Add sorting support for Node and subscription columns

This commit is contained in:
web@ppanel 2024-12-21 16:30:43 +07:00
parent 5737331067
commit 27924b0fdb
4 changed files with 192 additions and 108 deletions

View File

@ -8,6 +8,7 @@ import {
deleteNode, deleteNode,
getNodeGroupList, getNodeGroupList,
getNodeList, getNodeList,
nodeSort,
updateNode, updateNode,
} from '@/services/admin/server'; } from '@/services/admin/server';
import { ConfirmButton } from '@repo/ui/confirm-button'; import { ConfirmButton } from '@repo/ui/confirm-button';
@ -230,6 +231,32 @@ export default function NodeTable() {
]; ];
}, },
}} }}
onSort={async (source, target, items) => {
const sourceIndex = items.findIndex((item) => String(item.id) === source);
const targetIndex = items.findIndex((item) => String(item.id) === target);
const originalSorts = items.map((item) => ({ id: item.id, sort: item.sort || item.id }));
const [movedItem] = items.splice(sourceIndex, 1);
items.splice(targetIndex, 0, movedItem!);
const updatedItems = items.map((item) => {
const originalSort = originalSorts.find((sortItem) => sortItem.id === item.id)?.sort;
return {
...item,
sort: originalSort !== undefined ? originalSort : item.sort,
};
});
nodeSort({
sort: updatedItems.map((item) => {
return {
id: item.id,
sort: item.sort,
};
}),
});
return updatedItems;
}}
/> />
); );
} }

View File

@ -8,6 +8,7 @@ import {
deleteSubscribe, deleteSubscribe,
getSubscribeGroupList, getSubscribeGroupList,
getSubscribeList, getSubscribeList,
subscribeSort,
updateSubscribe, updateSubscribe,
} from '@/services/admin/subscribe'; } from '@/services/admin/subscribe';
import { ConfirmButton } from '@repo/ui/confirm-button'; import { ConfirmButton } from '@repo/ui/confirm-button';
@ -237,6 +238,32 @@ export default function SubscribeTable() {
/>, />,
], ],
}} }}
onSort={async (source, target, items) => {
const sourceIndex = items.findIndex((item) => String(item.id) === source);
const targetIndex = items.findIndex((item) => String(item.id) === target);
const originalSorts = items.map((item) => ({ id: item.id, sort: item.sort || item.id }));
const [movedItem] = items.splice(sourceIndex, 1);
items.splice(targetIndex, 0, movedItem!);
const updatedItems = items.map((item) => {
const originalSort = originalSorts.find((sortItem) => sortItem.id === item.id)?.sort;
return {
...item,
sort: originalSort !== undefined ? originalSort : item.sort,
};
});
subscribeSort({
sort: updatedItems.map((item) => {
return {
id: item.id,
sort: item.sort,
};
}),
});
return updatedItems;
}}
/> />
); );
} }

View File

@ -1,20 +1,7 @@
'use client'; 'use client';
import { import { DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
closestCenter, import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
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';
@ -40,6 +27,7 @@ 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'; import { SortableRow } from './sortable-row';
import { ProTableWrapper } from './wrapper';
export interface ProTableProps<TData, TValue> { export interface ProTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
@ -70,7 +58,11 @@ export interface ProTableProps<TData, TValue> {
selectedRowsText: (total: number) => string; selectedRowsText: (total: number) => string;
}>; }>;
empty?: React.ReactNode; empty?: React.ReactNode;
onSort?: (newOrder: TData[]) => void; onSort?: (
sourceId: string | number,
targetId: string | number | null,
items: TData[],
) => Promise<TData[]>;
} }
export interface ProTableActions { export interface ProTableActions {
@ -120,7 +112,13 @@ export function ProTable<
] ]
: []), : []),
...(actions?.batchRender ? [createSelectColumn<TData, TValue>()] : []), ...(actions?.batchRender ? [createSelectColumn<TData, TValue>()] : []),
...columns, ...columns.map(
(column) =>
({
enableSorting: false,
...column,
}) as ColumnDef<TData, TValue>,
),
...(actions?.render ...(actions?.render
? ([ ? ([
{ {
@ -141,7 +139,6 @@ export function ProTable<
] 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(),
@ -207,22 +204,11 @@ export function ProTable<
}), }),
); );
const handleDragEnd = (event: DragEndEvent) => { const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
if (onSort) {
if (active?.id !== over?.id) { const items = await onSort(active.id, over?.id || null, data);
setData((items) => { 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;
});
} }
}; };
@ -272,80 +258,70 @@ export function ProTable<
width: size?.width, width: size?.width,
}} }}
> >
<Table className='w-full'> <ProTableWrapper data={data} setData={setData} onSort={onSort}>
<TableHeader> <Table className='w-full'>
{table.getHeaderGroups().map((headerGroup) => ( <TableHeader>
<TableRow key={headerGroup.id}> {table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => ( <TableRow key={headerGroup.id}>
<TableHead key={header.id} className={getTableHeaderClass(header.column.id)}> {headerGroup.headers.map((header) => (
<ColumnHeader <TableHead key={header.id} className={getTableHeaderClass(header.column.id)}>
header={header} <ColumnHeader
text={{ header={header}
asc: texts?.asc, text={{
desc: texts?.desc, asc: texts?.asc,
hide: texts?.hide, desc: texts?.desc,
}} hide: texts?.hide,
/> }}
</TableHead> />
))} </TableHead>
</TableRow> ))}
))} </TableRow>
</TableHeader> ))}
<TableBody> </TableHeader>
{table.getRowModel()?.rows?.length ? ( <TableBody>
onSort ? ( {table.getRowModel()?.rows?.length ? (
<DndContext onSort ? (
sensors={sensors} table.getRowModel().rows.map((row) => (
collisionDetection={closestCenter} <SortableRow
onDragEnd={handleDragEnd} key={row.original.id ? String(row.original.id) : String(row.index)}
> id={row.original.id ? String(row.original.id) : String(row.index)}
<SortableContext data-state={row.getIsSelected() && 'selected'}
items={table.getRowModel()?.rows.map((row) => { isSortable
return String(row.original.id); >
})} {row
strategy={verticalListSortingStrategy} .getVisibleCells()
> .filter((cell) => {
{table.getRowModel().rows.map((row) => ( return cell.column.id !== 'sortable';
<SortableRow })
key={row.original.id ? String(row.original.id) : String(row.index)} .map((cell) => (
id={row.original.id ? String(row.original.id) : String(row.index)} <TableCell key={cell.id} className={getTableCellClass(cell.column.id)}>
data-state={row.getIsSelected() && 'selected'} {flexRender(cell.column.columnDef.cell, cell.getContext())}
isSortable </TableCell>
> ))}
{row </SortableRow>
.getVisibleCells() ))
.filter((cell) => { ) : (
return cell.column.id !== 'sortable'; table.getRowModel().rows.map((row) => (
}) <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
.map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className={getTableCellClass(cell.column.id)}> <TableCell key={cell.id} className={getTableCellClass(cell.column.id)}>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}
</SortableRow> </TableRow>
))} ))
</SortableContext> )
</DndContext>
) : ( ) : (
table.getRowModel().rows.map((row) => ( <TableRow>
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}> <TableCell colSpan={columns.length + 2} className='py-24'>
{row.getVisibleCells().map((cell) => ( {empty || <Empty />}
<TableCell key={cell.id} className={getTableCellClass(cell.column.id)}> </TableCell>
{flexRender(cell.column.columnDef.cell, cell.getContext())} </TableRow>
</TableCell> )}
))} </TableBody>
</TableRow> </Table>
)) </ProTableWrapper>
)
) : (
<TableRow>
<TableCell colSpan={columns.length + 2} className='py-24'>
{empty || <Empty />}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{loading && ( {loading && (
<div className='bg-muted/80 absolute top-0 z-20 flex h-full w-full items-center justify-center'> <div className='bg-muted/80 absolute top-0 z-20 flex h-full w-full items-center justify-center'>
<Loader className='h-4 w-4 animate-spin' /> <Loader className='h-4 w-4 animate-spin' />
@ -390,7 +366,7 @@ function createSelectColumn<TData, TValue>(): ColumnDef<TData, TValue> {
} }
function getTableHeaderClass(columnId: string) { function getTableHeaderClass(columnId: string) {
if (columnId === 'selected') { if (['sortable', 'selected'].includes(columnId)) {
return 'sticky left-0 z-10 bg-background shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)] [&:has([role=checkbox])]:pr-2'; return 'sticky left-0 z-10 bg-background shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)] [&:has([role=checkbox])]:pr-2';
} else if (columnId === 'actions') { } else if (columnId === 'actions') {
return 'sticky right-0 z-10 text-right bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'; return 'sticky right-0 z-10 text-right bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]';
@ -399,7 +375,7 @@ function getTableHeaderClass(columnId: string) {
} }
function getTableCellClass(columnId: string) { function getTableCellClass(columnId: string) {
if (columnId === 'selected') { if (['sortable', 'selected'].includes(columnId)) {
return 'sticky left-0 bg-background shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)]'; return 'sticky left-0 bg-background shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)]';
} else if (columnId === 'actions') { } else if (columnId === 'actions') {
return 'sticky right-0 bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'; return 'sticky right-0 bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]';

View File

@ -0,0 +1,54 @@
import {
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
export function ProTableWrapper<TData extends { id?: string }, TValue>({
children,
onSort,
data,
setData,
}: {
children: React.ReactNode;
onSort?: (
sourceId: string | number,
targetId: string | number | null,
items: TData[],
) => Promise<TData[]>;
data: TData[];
setData: React.Dispatch<React.SetStateAction<TData[]>>;
}) {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (onSort) {
const updatedData = await onSort(active.id, over?.id || null, data);
setData(updatedData);
}
};
if (!onSort) return children;
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext
items={data.map((item) => String(item.id))}
strategy={verticalListSortingStrategy}
>
{children}
</SortableContext>
</DndContext>
);
}