feat(tracker-web): item-detail ActionMenu + Timeline comments (UX-12.2)
Move the Edit/Delete item actions into a shared ActionMenu in the page header and render the comment history with the shared Timeline. Document UX-12.3 as data-gated/deferred: descriptions and comments are plain text with no backend HTML sanitization, so RichTextEditor is intentionally not adopted yet. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
ddf25cf501
commit
32dac7d466
@ -220,12 +220,19 @@ pnpm build # final gate
|
|||||||
(SegmentedControl now drives the roadmap board/list toggle — the app's only board↔list
|
(SegmentedControl now drives the roadmap board/list toggle — the app's only board↔list
|
||||||
toggle — replacing the bespoke `blue-600` buttons; `Tooltip` wraps the truncated board
|
toggle — replacing the bespoke `blue-600` buttons; `Tooltip` wraps the truncated board
|
||||||
card titles. e2e toggle selector updated button→radio. UX-12.1 verified: tc/lint/test 162 ✓/build/e2e 18 ✓)
|
card titles. e2e toggle selector updated button→radio. UX-12.1 verified: tc/lint/test 162 ✓/build/e2e 18 ✓)
|
||||||
- [ ] **12.2** Item detail: move row/item actions into an `ActionMenu`, and render the item's
|
- [x] **12.2** Item detail: move row/item actions into an `ActionMenu`, and render the item's
|
||||||
activity/comment history with `Timeline`.
|
activity/comment history with `Timeline`.
|
||||||
|
(Edit + Delete now live in an `ActionMenu` in the page header; comments render via
|
||||||
|
`Timeline`. UX-12.2 verified: tc/lint/test 162 ✓/build/e2e 18 ✓.)
|
||||||
- [ ] **12.3** _(stretch — needs HTML-capable description/comment storage)_ Swap the plain
|
- [ ] **12.3** _(stretch — needs HTML-capable description/comment storage)_ Swap the plain
|
||||||
description/comment `<textarea>` for `RichTextEditor`, and render saved content with
|
description/comment `<textarea>` for `RichTextEditor`, and render saved content with
|
||||||
`RichTextViewer`. **Only do this if** the backend stores/returns rich HTML safely;
|
`RichTextViewer`. **Only do this if** the backend stores/returns rich HTML safely;
|
||||||
otherwise leave `- [ ]` with a note. **Verify per task:** `pnpm typecheck && pnpm lint && pnpm build`.
|
otherwise leave `- [ ]` with a note. **Verify per task:** `pnpm typecheck && pnpm lint && pnpm build`.
|
||||||
|
DEFERRED (data-gated): `TrackerItem.description` and `Comment.body` are plain `string`s
|
||||||
|
rendered with `whitespace-pre-wrap`; the `/api/tracker/*` proxy does not store or sanitize
|
||||||
|
rich HTML. Adopting `RichTextEditor` would persist HTML with no backend sanitization (XSS
|
||||||
|
risk) and mismatch the plain-text model, so `@bytelyst/rich-text` is intentionally not
|
||||||
|
adopted until the backend supports safe rich HTML. No dep added.
|
||||||
|
|
||||||
## UX-13 — Notifications surface via `@bytelyst/notifications-ui` (stretch / data-gated)
|
## UX-13 — Notifications surface via `@bytelyst/notifications-ui` (stretch / data-gated)
|
||||||
|
|
||||||
@ -261,7 +268,7 @@ pnpm build # final gate
|
|||||||
|
|
||||||
```
|
```
|
||||||
Core : UX-1 ✅ UX-2 ⬜ UX-3 ⬜ UX-4 ⬜ UX-5 ⬜ UX-6 ⬜ UX-7 ⬜ UX-8 ⬜
|
Core : UX-1 ✅ UX-2 ⬜ UX-3 ⬜ UX-4 ⬜ UX-5 ⬜ UX-6 ⬜ UX-7 ⬜ UX-8 ⬜
|
||||||
Expand : UX-9 ✅ UX-10 ✅ UX-11 ✅ UX-12 ⬜ UX-13 ⬜ (stretch: 12.3, 13.*)
|
Expand : UX-9 ✅ UX-10 ✅ UX-11 ✅ UX-12 ✅ UX-13 ⬜ (stretch: 12.3, 13.*)
|
||||||
```
|
```
|
||||||
|
|
||||||
**UX-1 is done** (token bridge + Primitives adapter, commit `dc01dd02`) — the `--bl-*` bridge is
|
**UX-1 is done** (token bridge + Primitives adapter, commit `dc01dd02`) — the `--bl-*` bridge is
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
|
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
|
||||||
|
import { ActionMenu, Timeline } from '@/components/ui/Primitives';
|
||||||
import { useAuth } from '@/lib/auth-context';
|
import { useAuth } from '@/lib/auth-context';
|
||||||
import {
|
import {
|
||||||
getItem,
|
getItem,
|
||||||
updateItem,
|
updateItem,
|
||||||
updateItemStatus,
|
updateItemStatus,
|
||||||
|
deleteItem,
|
||||||
listComments,
|
listComments,
|
||||||
addComment,
|
addComment,
|
||||||
toggleVote,
|
toggleVote,
|
||||||
@ -21,6 +23,7 @@ const VISIBILITIES = ['internal', 'public'] as const;
|
|||||||
|
|
||||||
export default function ItemDetailPage() {
|
export default function ItemDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const router = useRouter();
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
|
||||||
const [item, setItem] = useState<TrackerItem | null>(null);
|
const [item, setItem] = useState<TrackerItem | null>(null);
|
||||||
@ -107,6 +110,24 @@ export default function ItemDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
if (!confirm('Delete this item? This cannot be undone.')) return;
|
||||||
|
try {
|
||||||
|
await deleteItem(id);
|
||||||
|
router.push('/dashboard/items');
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to delete');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
if (!item) return;
|
||||||
|
setEditTitle(item.title);
|
||||||
|
setEditDescription(item.description);
|
||||||
|
setEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex min-h-[400px] max-w-3xl items-center justify-center">
|
<div className="mx-auto flex min-h-[400px] max-w-3xl items-center justify-center">
|
||||||
@ -125,16 +146,13 @@ export default function ItemDetailPage() {
|
|||||||
]}
|
]}
|
||||||
actions={
|
actions={
|
||||||
editing ? undefined : (
|
editing ? undefined : (
|
||||||
<button
|
<ActionMenu
|
||||||
onClick={() => {
|
label="Item actions"
|
||||||
setEditTitle(item.title);
|
items={[
|
||||||
setEditDescription(item.description);
|
{ id: 'edit', label: 'Edit', onSelect: startEdit },
|
||||||
setEditing(true);
|
{ id: 'delete', label: 'Delete', destructive: true, onSelect: handleDelete },
|
||||||
}}
|
]}
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent"
|
/>
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -262,15 +280,15 @@ export default function ItemDetailPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold">Comments ({comments.length})</h2>
|
<h2 className="text-lg font-semibold">Comments ({comments.length})</h2>
|
||||||
|
|
||||||
{comments.map(c => (
|
<Timeline
|
||||||
<div key={c.id} className="rounded-lg border border-border bg-card p-4">
|
emptyLabel="No comments yet."
|
||||||
<div className="mb-2 flex items-center justify-between text-xs text-muted-foreground">
|
items={comments.map(c => ({
|
||||||
<span>{c.authorEmail || c.authorId}</span>
|
id: c.id,
|
||||||
<span>{new Date(c.createdAt).toLocaleString()}</span>
|
title: c.authorEmail || c.authorId,
|
||||||
</div>
|
meta: new Date(c.createdAt).toLocaleString(),
|
||||||
<p className="whitespace-pre-wrap text-sm">{c.body}</p>
|
description: <span className="whitespace-pre-wrap">{c.body}</span>,
|
||||||
</div>
|
}))}
|
||||||
))}
|
/>
|
||||||
|
|
||||||
<form onSubmit={handleAddComment} className="space-y-2">
|
<form onSubmit={handleAddComment} className="space-y-2">
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user