✨ 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,
|
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;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)]';
|
||||||
|
|||||||
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