fix: improvements in the dashboard
This commit is contained in:
@@ -5,6 +5,7 @@ import type {
|
||||
} from '@tanstack/react-table';
|
||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||
import { useState } from 'react';
|
||||
import { useLocalStorage } from 'usehooks-ts';
|
||||
|
||||
export const useDataTablePagination = (pageSize = 10) => {
|
||||
const [page, setPage] = useQueryState(
|
||||
@@ -23,12 +24,29 @@ export const useDataTablePagination = (pageSize = 10) => {
|
||||
|
||||
export const useDataTableColumnVisibility = <TData,>(
|
||||
columns: ColumnDef<TData>[],
|
||||
persistentKey: string,
|
||||
) => {
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||
const [columnVisibility, setColumnVisibility] = useLocalStorage<
|
||||
Record<string, boolean>
|
||||
>(
|
||||
`@op:${persistentKey}-column-visibility`,
|
||||
columns.reduce((acc, column) => {
|
||||
acc[column.id!] = column.meta?.hidden ?? false;
|
||||
// Use accessorKey as fallback if id is not provided
|
||||
const columnId = column.id || (column as any).accessorKey;
|
||||
if (columnId) {
|
||||
acc[columnId] =
|
||||
typeof column.meta?.hidden === 'boolean'
|
||||
? !column.meta?.hidden
|
||||
: true;
|
||||
}
|
||||
return acc;
|
||||
}, {} as VisibilityState),
|
||||
);
|
||||
return { columnVisibility, setColumnVisibility };
|
||||
|
||||
const [columnOrder, setColumnOrder] = useLocalStorage<string[]>(
|
||||
`@op:${persistentKey}-column-order`,
|
||||
columns.map((column) => column.id!),
|
||||
);
|
||||
|
||||
return { columnVisibility, setColumnVisibility, columnOrder, setColumnOrder };
|
||||
};
|
||||
|
||||
@@ -13,28 +13,162 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import type { Table } from '@tanstack/react-table';
|
||||
import { Check, ChevronsUpDown, Settings2Icon } from 'lucide-react';
|
||||
import {
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
GripVertical,
|
||||
RotateCcw,
|
||||
Settings2Icon,
|
||||
} from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
interface DataTableViewOptionsProps<TData> {
|
||||
table: Table<TData>;
|
||||
}
|
||||
|
||||
interface SortableColumnItemProps {
|
||||
column: any;
|
||||
onToggleVisibility: () => void;
|
||||
}
|
||||
|
||||
function SortableColumnItem({
|
||||
column,
|
||||
onToggleVisibility,
|
||||
}: SortableColumnItemProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: column.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn('flex items-center gap-2', isDragging && 'opacity-50')}
|
||||
onSelect={onToggleVisibility}
|
||||
>
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="cursor-grab active:cursor-grabbing p-1 hover:bg-muted rounded"
|
||||
>
|
||||
<GripVertical className="size-3 text-muted-foreground" />
|
||||
</div>
|
||||
<span className="truncate flex-1">
|
||||
{typeof column.columnDef.header === 'string'
|
||||
? column.columnDef.header
|
||||
: (column.columnDef.meta?.label ?? column.id)}
|
||||
</span>
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto size-4 shrink-0',
|
||||
column.getIsVisible() ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function DataTableViewOptions<TData>({
|
||||
table,
|
||||
}: DataTableViewOptionsProps<TData>) {
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
||||
),
|
||||
[table],
|
||||
const allColumns = table.getAllColumns();
|
||||
const filterableColumns = allColumns.filter(
|
||||
(column) => typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
||||
);
|
||||
|
||||
// Use the column order from the table state (managed by useDataTableColumnVisibility)
|
||||
const columns = React.useMemo(() => {
|
||||
const columnMap = new Map(filterableColumns.map((col) => [col.id, col]));
|
||||
const orderedColumns: typeof filterableColumns = [];
|
||||
const currentColumnOrder = table.getState().columnOrder;
|
||||
|
||||
// Add columns in the current table order
|
||||
currentColumnOrder.forEach((columnId) => {
|
||||
const column = columnMap.get(columnId);
|
||||
if (column) {
|
||||
orderedColumns.push(column);
|
||||
columnMap.delete(columnId);
|
||||
}
|
||||
});
|
||||
|
||||
// Add any new columns that weren't in the current order
|
||||
columnMap.forEach((column) => {
|
||||
orderedColumns.push(column);
|
||||
});
|
||||
|
||||
return orderedColumns;
|
||||
}, [filterableColumns, table]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
const oldIndex = columns.findIndex((column) => column.id === active.id);
|
||||
const newIndex = columns.findIndex((column) => column.id === over?.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
// Reorder the columns in the table
|
||||
const newColumns = [...columns];
|
||||
const [removed] = newColumns.splice(oldIndex, 1);
|
||||
newColumns.splice(newIndex, 0, removed);
|
||||
|
||||
// Update the table column order (this will automatically persist via useDataTableColumnVisibility)
|
||||
table.setColumnOrder(newColumns.map((col) => col.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
// Reset column visibility to default (all visible)
|
||||
allColumns.forEach((column) => {
|
||||
if (column.getCanHide()) {
|
||||
column.toggleVisibility(
|
||||
typeof column.columnDef.meta?.hidden === 'boolean'
|
||||
? !column.columnDef.meta?.hidden
|
||||
: true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset column order to default (this will automatically persist via useDataTableColumnVisibility)
|
||||
const defaultOrder = filterableColumns.map((col) => col.id);
|
||||
table.setColumnOrder(defaultOrder);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -50,32 +184,41 @@ export function DataTableViewOptions<TData>({
|
||||
<ChevronsUpDown className="opacity-50 ml-2 size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-44 p-0">
|
||||
<PopoverContent align="end" className="w-52 p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search columns..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No columns found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{columns.map((column) => (
|
||||
<CommandItem
|
||||
key={column.id}
|
||||
onSelect={() =>
|
||||
column.toggleVisibility(!column.getIsVisible())
|
||||
}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={columns.map((col) => col.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<span className="truncate">
|
||||
{typeof column.columnDef.header === 'string'
|
||||
? column.columnDef.header
|
||||
: (column.columnDef.meta?.label ?? column.id)}
|
||||
</span>
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto size-4 shrink-0',
|
||||
column.getIsVisible() ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
{columns.map((column) => (
|
||||
<SortableColumnItem
|
||||
key={column.id}
|
||||
column={column}
|
||||
onToggleVisibility={() =>
|
||||
column.toggleVisibility(!column.getIsVisible())
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</CommandGroup>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={handleReset}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<RotateCcw className="size-4 mr-2" />
|
||||
Reset to default
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
useQueryStates,
|
||||
} from 'nuqs';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useDataTableColumnVisibility } from './data-table-hooks';
|
||||
|
||||
const nuqsOptions: Options = {
|
||||
shallow: true,
|
||||
@@ -35,11 +36,13 @@ export function useTable<TData>({
|
||||
pageSize,
|
||||
data,
|
||||
loading,
|
||||
name,
|
||||
}: {
|
||||
columns: ColumnDef<TData>[];
|
||||
pageSize: number;
|
||||
data: TData[];
|
||||
loading: boolean;
|
||||
name: string;
|
||||
}) {
|
||||
const [page, setPage] = useQueryState(
|
||||
'page',
|
||||
@@ -54,6 +57,9 @@ export function useTable<TData>({
|
||||
pageSize: perPage,
|
||||
};
|
||||
|
||||
const { columnVisibility, setColumnVisibility, columnOrder, setColumnOrder } =
|
||||
useDataTableColumnVisibility(columns, name);
|
||||
|
||||
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
|
||||
left: [
|
||||
...columns
|
||||
@@ -149,6 +155,9 @@ export function useTable<TData>({
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
meta: {
|
||||
name,
|
||||
},
|
||||
columns,
|
||||
data: useMemo(
|
||||
() =>
|
||||
@@ -181,7 +190,11 @@ export function useTable<TData>({
|
||||
pagination,
|
||||
columnPinning,
|
||||
columnFilters: loading ? [] : columnFilters,
|
||||
columnOrder,
|
||||
columnVisibility,
|
||||
},
|
||||
onColumnOrderChange: setColumnOrder,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onColumnPinningChange: setColumnPinning,
|
||||
onColumnFiltersChange: (updaterOrValue: Updater<ColumnFiltersState>) => {
|
||||
setColumnFilters((prev) => {
|
||||
|
||||
Reference in New Issue
Block a user