feat(tracker-web): adopt @bytelyst/data-table for items list (Wave 9.C.9)
Replaces the bespoke <table> in /dashboard/items with <DataTable> (TanStack- powered sorting + client pagination), keeping the existing badge cells, title link, labels and delete action via ColumnDef cell renderers. Server-side type/status/priority/search filters retained (enableFilter=false on the table). Verified: tsc --noEmit clean; vitest 31/31; next build --webpack succeeds (/dashboard/items compiles).
This commit is contained in:
parent
7e1a2ad660
commit
0985969377
@ -27,6 +27,7 @@
|
||||
"@bytelyst/api-client": "workspace:*",
|
||||
"@bytelyst/config": "workspace:*",
|
||||
"@bytelyst/dashboard-components": "workspace:*",
|
||||
"@bytelyst/data-table": "workspace:*",
|
||||
"@bytelyst/errors": "workspace:*",
|
||||
"@bytelyst/telemetry-client": "workspace:*",
|
||||
"@bytelyst/logger": "workspace:*",
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { DataTable, type ColumnDef } from '@bytelyst/data-table';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { listItems, createItem, deleteItem, type TrackerItem } from '@/lib/tracker-client';
|
||||
|
||||
@ -87,15 +88,105 @@ export default function ItemsListPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Delete this item?')) return;
|
||||
try {
|
||||
await deleteItem(id);
|
||||
fetchItems();
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete');
|
||||
}
|
||||
};
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
if (!confirm('Delete this item?')) return;
|
||||
try {
|
||||
await deleteItem(id);
|
||||
fetchItems();
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete');
|
||||
}
|
||||
},
|
||||
[fetchItems]
|
||||
);
|
||||
|
||||
const columns = useMemo<ColumnDef<TrackerItem, unknown>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'title',
|
||||
accessorKey: 'title',
|
||||
header: 'Title',
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
href={`/dashboard/items/${item.id}`}
|
||||
className="font-medium text-foreground hover:text-primary hover:underline"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
{item.labels.length > 0 && (
|
||||
<div className="mt-1 flex gap-1">
|
||||
{item.labels.map(l => (
|
||||
<span
|
||||
key={l}
|
||||
className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
|
||||
>
|
||||
{l}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
accessorKey: 'type',
|
||||
header: 'Type',
|
||||
cell: ({ row }) => (
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${TYPE_BADGE[row.original.type] || ''}`}
|
||||
>
|
||||
{row.original.type}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${STATUS_BADGE[row.original.status] || ''}`}
|
||||
>
|
||||
{row.original.status.replace(/_/g, ' ')}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'priority',
|
||||
accessorKey: 'priority',
|
||||
header: 'Priority',
|
||||
cell: ({ row }) => (
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${PRIORITY_BADGE[row.original.priority] || ''}`}
|
||||
>
|
||||
{row.original.priority}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ id: 'voteCount', accessorKey: 'voteCount', header: 'Votes' },
|
||||
{ id: 'commentCount', accessorKey: 'commentCount', header: 'Comments' },
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<button
|
||||
onClick={() => handleDelete(row.original.id)}
|
||||
className="text-xs text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[handleDelete]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@ -163,88 +254,21 @@ export default function ItemsListPage() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Items table */}
|
||||
{/* Items table — @bytelyst/data-table (Wave 9.C.9) */}
|
||||
{loading ? (
|
||||
<div className="text-muted-foreground">Loading...</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="rounded-xl border border-border bg-card p-12 text-center text-muted-foreground">
|
||||
No items found. Create one to get started.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-xl border border-border">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-medium">Title</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Type</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Status</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Priority</th>
|
||||
<th className="px-4 py-3 text-center font-medium">Votes</th>
|
||||
<th className="px-4 py-3 text-center font-medium">Comments</th>
|
||||
<th className="px-4 py-3 text-right font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{items.map(item => (
|
||||
<tr key={item.id} className="hover:bg-muted/30 transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<Link
|
||||
href={`/dashboard/items/${item.id}`}
|
||||
className="font-medium text-foreground hover:text-primary hover:underline"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
{item.labels.length > 0 && (
|
||||
<div className="mt-1 flex gap-1">
|
||||
{item.labels.map(l => (
|
||||
<span
|
||||
key={l}
|
||||
className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
|
||||
>
|
||||
{l}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${TYPE_BADGE[item.type] || ''}`}
|
||||
>
|
||||
{item.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${STATUS_BADGE[item.status] || ''}`}
|
||||
>
|
||||
{item.status.replace(/_/g, ' ')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${PRIORITY_BADGE[item.priority] || ''}`}
|
||||
>
|
||||
{item.priority}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-muted-foreground">{item.voteCount}</td>
|
||||
<td className="px-4 py-3 text-center text-muted-foreground">
|
||||
{item.commentCount}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button
|
||||
onClick={() => handleDelete(item.id)}
|
||||
className="text-xs text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataTable
|
||||
ariaLabel="Tracker items"
|
||||
columns={columns}
|
||||
data={items}
|
||||
getRowId={item => item.id}
|
||||
enableFilter={false}
|
||||
enableSorting
|
||||
enablePagination
|
||||
pageSize={15}
|
||||
emptyState="No items found. Create one to get started."
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Create modal */}
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -249,6 +249,9 @@ importers:
|
||||
'@bytelyst/dashboard-components':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/dashboard-components
|
||||
'@bytelyst/data-table':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/data-table
|
||||
'@bytelyst/errors':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/errors
|
||||
|
||||
Loading…
Reference in New Issue
Block a user