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/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:*",
|
||||||
|
|||||||
@ -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
3
pnpm-lock.yaml
generated
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user