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:
saravanakumardb1 2026-05-28 18:38:08 -07:00
parent 7e1a2ad660
commit 0985969377
3 changed files with 117 additions and 89 deletions

View File

@ -27,6 +27,7 @@
"@bytelyst/api-client": "workspace:*", "@bytelyst/api-client": "workspace:*",
"@bytelyst/config": "workspace:*", "@bytelyst/config": "workspace:*",
"@bytelyst/dashboard-components": "workspace:*", "@bytelyst/dashboard-components": "workspace:*",
"@bytelyst/data-table": "workspace:*",
"@bytelyst/errors": "workspace:*", "@bytelyst/errors": "workspace:*",
"@bytelyst/telemetry-client": "workspace:*", "@bytelyst/telemetry-client": "workspace:*",
"@bytelyst/logger": "workspace:*", "@bytelyst/logger": "workspace:*",

View File

@ -1,7 +1,8 @@
'use client'; 'use client';
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { DataTable, type ColumnDef } from '@bytelyst/data-table';
import { useAuth } from '@/lib/auth-context'; import { useAuth } from '@/lib/auth-context';
import { listItems, createItem, deleteItem, type TrackerItem } from '@/lib/tracker-client'; import { listItems, createItem, deleteItem, type TrackerItem } from '@/lib/tracker-client';
@ -87,15 +88,105 @@ export default function ItemsListPage() {
} }
}; };
const handleDelete = async (id: string) => { const handleDelete = useCallback(
if (!confirm('Delete this item?')) return; async (id: string) => {
try { if (!confirm('Delete this item?')) return;
await deleteItem(id); try {
fetchItems(); await deleteItem(id);
} catch (err: unknown) { fetchItems();
setError(err instanceof Error ? err.message : 'Failed to delete'); } 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
@ -163,88 +254,21 @@ export default function ItemsListPage() {
</select> </select>
</div> </div>
{/* Items table */} {/* Items table — @bytelyst/data-table (Wave 9.C.9) */}
{loading ? ( {loading ? (
<div className="text-muted-foreground">Loading...</div> <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"> <DataTable
<table className="w-full text-sm"> ariaLabel="Tracker items"
<thead className="bg-muted/50"> columns={columns}
<tr> data={items}
<th className="px-4 py-3 text-left font-medium">Title</th> getRowId={item => item.id}
<th className="px-4 py-3 text-left font-medium">Type</th> enableFilter={false}
<th className="px-4 py-3 text-left font-medium">Status</th> enableSorting
<th className="px-4 py-3 text-left font-medium">Priority</th> enablePagination
<th className="px-4 py-3 text-center font-medium">Votes</th> pageSize={15}
<th className="px-4 py-3 text-center font-medium">Comments</th> emptyState="No items found. Create one to get started."
<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>
)} )}
{/* Create modal */} {/* Create modal */}

3
pnpm-lock.yaml generated
View File

@ -249,6 +249,9 @@ importers:
'@bytelyst/dashboard-components': '@bytelyst/dashboard-components':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/dashboard-components version: link:../../packages/dashboard-components
'@bytelyst/data-table':
specifier: workspace:*
version: link:../../packages/data-table
'@bytelyst/errors': '@bytelyst/errors':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/errors version: link:../../packages/errors