fix: improvements in the dashboard

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-10-17 18:15:23 +02:00
parent c8bea685db
commit 077a47a263
29 changed files with 1133 additions and 526 deletions

View File

@@ -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 };
};

View File

@@ -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>

View File

@@ -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) => {