✨ feat(table): Add sorting support for Node and subscription columns
This commit is contained in:
parent
5737331067
commit
27924b0fdb
@ -8,6 +8,7 @@ import {
|
||||
deleteNode,
|
||||
getNodeGroupList,
|
||||
getNodeList,
|
||||
nodeSort,
|
||||
updateNode,
|
||||
} from '@/services/admin/server';
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
deleteSubscribe,
|
||||
getSubscribeGroupList,
|
||||
getSubscribeList,
|
||||
subscribeSort,
|
||||
updateSubscribe,
|
||||
} from '@/services/admin/subscribe';
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,20 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@shadcn/ui/alert';
|
||||
import { Button } from '@shadcn/ui/button';
|
||||
import { Checkbox } from '@shadcn/ui/checkbox';
|
||||
@ -40,6 +27,7 @@ import { ColumnHeader } from './column-header';
|
||||
import { ColumnToggle } from './column-toggle';
|
||||
import { Pagination } from './pagination';
|
||||
import { SortableRow } from './sortable-row';
|
||||
import { ProTableWrapper } from './wrapper';
|
||||
|
||||
export interface ProTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
@ -70,7 +58,11 @@ export interface ProTableProps<TData, TValue> {
|
||||
selectedRowsText: (total: number) => string;
|
||||
}>;
|
||||
empty?: React.ReactNode;
|
||||
onSort?: (newOrder: TData[]) => void;
|
||||
onSort?: (
|
||||
sourceId: string | number,
|
||||
targetId: string | number | null,
|
||||
items: TData[],
|
||||
) => Promise<TData[]>;
|
||||
}
|
||||
|
||||
export interface ProTableActions {
|
||||
@ -120,7 +112,13 @@ export function ProTable<
|
||||
]
|
||||
: []),
|
||||
...(actions?.batchRender ? [createSelectColumn<TData, TValue>()] : []),
|
||||
...columns,
|
||||
...columns.map(
|
||||
(column) =>
|
||||
({
|
||||
enableSorting: false,
|
||||
...column,
|
||||
}) as ColumnDef<TData, TValue>,
|
||||
),
|
||||
...(actions?.render
|
||||
? ([
|
||||
{
|
||||
@ -141,7 +139,6 @@ export function ProTable<
|
||||
] as ColumnDef<TData, TValue>[],
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
@ -207,22 +204,11 @@ export function ProTable<
|
||||
}),
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const handleDragEnd = async (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;
|
||||
});
|
||||
if (onSort) {
|
||||
const items = await onSort(active.id, over?.id || null, data);
|
||||
setData(items);
|
||||
}
|
||||
};
|
||||
|
||||
@ -272,80 +258,70 @@ export function ProTable<
|
||||
width: size?.width,
|
||||
}}
|
||||
>
|
||||
<Table className='w-full'>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id} className={getTableHeaderClass(header.column.id)}>
|
||||
<ColumnHeader
|
||||
header={header}
|
||||
text={{
|
||||
asc: texts?.asc,
|
||||
desc: texts?.desc,
|
||||
hide: texts?.hide,
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel()?.rows?.length ? (
|
||||
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>
|
||||
<ProTableWrapper data={data} setData={setData} onSort={onSort}>
|
||||
<Table className='w-full'>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id} className={getTableHeaderClass(header.column.id)}>
|
||||
<ColumnHeader
|
||||
header={header}
|
||||
text={{
|
||||
asc: texts?.asc,
|
||||
desc: texts?.desc,
|
||||
hide: texts?.hide,
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel()?.rows?.length ? (
|
||||
onSort ? (
|
||||
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>
|
||||
))
|
||||
) : (
|
||||
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>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
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'>
|
||||
{empty || <Empty />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length + 2} className='py-24'>
|
||||
{empty || <Empty />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ProTableWrapper>
|
||||
|
||||
{loading && (
|
||||
<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' />
|
||||
@ -390,7 +366,7 @@ function createSelectColumn<TData, TValue>(): ColumnDef<TData, TValue> {
|
||||
}
|
||||
|
||||
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';
|
||||
} 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)]';
|
||||
@ -399,7 +375,7 @@ function getTableHeaderClass(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)]';
|
||||
} else if (columnId === 'actions') {
|
||||
return 'sticky right-0 bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]';
|
||||
|
||||
54
packages/ui/src/pro-table/wrapper.tsx
Normal file
54
packages/ui/src/pro-table/wrapper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user