Issues list: show milestone on each row; replace the word 'comments' with a message-bubble indicator #380

Closed
opened 2026-09-12 15:14:56 +00:00 by crueber · 3 comments
Owner

What's requested

Two changes to the issues list rows (web/src/pages/Issues.jsx, row markup at lines 156-176):

  1. Show the milestone on each row, right-aligned in the meta span, between the comment count and the updated timestamp.
  2. Replace the word "comments" with a message-bubble emoji/icon — currently the row renders the count as plain text: {issue.comment_count} comments · <DateTime …/>.

Evidence

  • web/src/pages/Issues.jsx:172-174 — the right-aligned meta span renders only comment count (as the literal word "comments") and the updated datetime. No milestone.
  • The data is already on the wire: the list projection carries it — Card.Milestone *string at internal/issues/model.go:95 (JSON milestone), projected in cardOf (internal/issues/model.go:103-111). No backend change needed.
  • Milestone ids on cards are stored ids (<id:06x> hex), not titles. Title resolution already exists and is the convention to reuse: milestoneDisplay(milestones, id) in web/src/lib/milestones.js:29-36 returns {pending, text, unknown?} — pending renders a placeholder (never a bare id flash on cold load), a deleted milestone renders the bare id (same self-heal stance as unknown labels).
  • The issue detail page already consumes the repo milestone set for display: web/src/pages/Issue.jsx:65 (useData("milestones:{full}", …, TTL.milestones)). The list page will need the same fetch to resolve titles.

Architecture notes

  • Id→title resolution on a list page: rows render before the milestone set may have loaded. Use milestoneDisplay's pending state (placeholder or render-nothing) rather than flashing raw hex ids. The repo set is a 30s-TTL cached key (milestones:{full}), one extra fetch per page load — acceptable.
  • Rows are ccSWR-cached list windows; milestone display is a pure client-side projection over card data, so no ETag/cache-contract concern — the field already rides the cached payload.
  • Only render the milestone segment when the card has one (Card.milestone is nullable) — no empty chip for the common unset case. A muted chip/pill styled like the existing label-chip row language (but smaller, matching the xs meta text) is the suggested treatment; exact styling is implementer's call.
  • Message-bubble indicator: pick a message/speech-bubble emoji (or inline SVG if the codebase prefers icons) preceding the count. Keep it decorative (aria-hidden) and let the number stand alone, or keep an accessible label — implementer's call, but the literal word "comments" goes away.

Acceptance criteria

  • Issues list rows show the milestone title right-aligned, ordered: comment count → milestone → updated time (i.e. milestone between comments and time).
  • Rows without a milestone render no milestone element (no placeholder, no empty chip).
  • Milestone with a loaded repo set shows the title; deleted/unknown id falls back to the bare id; cold-load pending state never flashes a raw hex id.
  • Clicking the milestone on a row navigates to the filtered list (/issues?milestone=<id>), matching the issue-detail sidebar link behavior (web/src/pages/Issue.jsx:556).
  • The word "comments" no longer appears in the row; the count is preceded by a message-bubble indicator.
  • Long milestone titles truncate/wrap sanely within the flex row (min-w-0 / truncation context, same safety as the title).
  • No backend changes.
## What's requested Two changes to the issues list rows (`web/src/pages/Issues.jsx`, row markup at lines 156-176): 1. **Show the milestone on each row**, right-aligned in the meta span, between the comment count and the updated timestamp. 2. **Replace the word "comments" with a message-bubble emoji/icon** — currently the row renders the count as plain text: `{issue.comment_count} comments · <DateTime …/>`. ## Evidence - `web/src/pages/Issues.jsx:172-174` — the right-aligned meta span renders only comment count (as the literal word "comments") and the updated datetime. No milestone. - The data is already on the wire: the list projection carries it — `Card.Milestone *string` at `internal/issues/model.go:95` (JSON `milestone`), projected in `cardOf` (`internal/issues/model.go:103-111`). **No backend change needed.** - Milestone ids on cards are stored ids (`<id:06x>` hex), not titles. Title resolution already exists and is the convention to reuse: `milestoneDisplay(milestones, id)` in `web/src/lib/milestones.js:29-36` returns `{pending, text, unknown?}` — pending renders a placeholder (never a bare id flash on cold load), a deleted milestone renders the bare id (same self-heal stance as unknown labels). - The issue detail page already consumes the repo milestone set for display: `web/src/pages/Issue.jsx:65` (`useData("milestones:{full}", …, TTL.milestones)`). The list page will need the same fetch to resolve titles. ## Architecture notes - **Id→title resolution on a list page**: rows render before the milestone set may have loaded. Use `milestoneDisplay`'s pending state (placeholder or render-nothing) rather than flashing raw hex ids. The repo set is a 30s-TTL cached key (`milestones:{full}`), one extra fetch per page load — acceptable. - Rows are `ccSWR`-cached list windows; milestone display is a pure client-side projection over card data, so no ETag/cache-contract concern — the field already rides the cached payload. - Only render the milestone segment when the card has one (`Card.milestone` is nullable) — no empty chip for the common unset case. A muted chip/pill styled like the existing label-chip row language (but smaller, matching the xs meta text) is the suggested treatment; exact styling is implementer's call. - Message-bubble indicator: pick a message/speech-bubble emoji (or inline SVG if the codebase prefers icons) preceding the count. Keep it decorative (`aria-hidden`) and let the number stand alone, or keep an accessible label — implementer's call, but the literal word "comments" goes away. ## Acceptance criteria - [ ] Issues list rows show the milestone title right-aligned, ordered: comment count → milestone → updated time (i.e. milestone between comments and time). - [ ] Rows without a milestone render no milestone element (no placeholder, no empty chip). - [ ] Milestone with a loaded repo set shows the title; deleted/unknown id falls back to the bare id; cold-load pending state never flashes a raw hex id. - [ ] Clicking the milestone on a row navigates to the filtered list (`/issues?milestone=<id>`), matching the issue-detail sidebar link behavior (`web/src/pages/Issue.jsx:556`). - [ ] The word "comments" no longer appears in the row; the count is preceded by a message-bubble indicator. - [ ] Long milestone titles truncate/wrap sanely within the flex row (min-w-0 / truncation context, same safety as the title). - [ ] No backend changes.
crueber added this to the v1 milestone 2026-09-12 15:15:11 +00:00
Author
Owner

PR #386 (fix/issue-380) ready for review: milestone chip on rows (comments -> milestone -> time) + bubble count, no backend change. node --test 756 pass / 0 fail, vite+esbuild green. Browser proof open (shared-daemon loopback guard).

PR #386 (fix/issue-380) ready for review: milestone chip on rows (comments -> milestone -> time) + bubble count, no backend change. node --test 756 pass / 0 fail, vite+esbuild green. Browser proof open (shared-daemon loopback guard).
Author
Owner

REVIEW — PR #386 (fix/issue-380, 3b3229c): 3 files, +125/-2 (Issues.jsx, issues-row-milestone.test.js, 12_web_ui.md decision). No backend change, no new deps, no Go touched (fmt/vet n/a).

Acceptance criteria, all verified against the diff:

  1. ORDER count -> milestone -> time — PASS. Issues.jsx meta reads count-span, chip, DateTime; index-order pinned by the new test.
  2. UNSET -> no element — PASS. <Show when={issue.milestone != null}>; no placeholder/chip. (== null covers both null and undefined.)
  3. PENDING -> placeholder, never raw hex; UNKNOWN -> bare id — PASS. milestoneDisplay(getMilestoneSet()?.milestones, issue.milestone) (milestones.js:29-34); pending renders muted ellipsis placeholder; unknown falls back to bare id — same self-heal stance as labels, consistent. Raw id never renders as text (only inside the filter href).
  4. CLICK -> /issues?milestone= — PASS, and slightly better than asked: uses shared milestoneFilterHref (milestones.js:70-72), byte-identical to the Issue.jsx:556 sidebar inline template, also used by Milestones.jsx. Single source, same destination.
  5. WORD 'comments' GONE from visible row text — PASS. Old '{count} comments' literal removed; 'comments' survives only inside title/aria-label attrs (explicitly allowed: 'keep an accessible label — implementer's call'). Pulls.jsx rows never had the word (out of scope anyway). NOTE (not blocking): Issue.jsx:406 thread header still renders '{count} comments' — thread page, outside this issue's list-row scope.
  6. BUBBLE decorative + label — PASS. aria-hidden bubble span with aria-label + title 'N comments'. Nit (not blocking): aria-label on a role-less span is ignored by some AT, but the count text itself remains exposed, so nothing is lost.
  7. TRUNCATION — PASS. chip max-w-40 truncate + full-title tooltip; row keeps min-w-0 flex-wrap context (#334 safety). Tailwind 'truncate' carries overflow/ellipsis/nowrap.
  8. FETCH — PASS, as designed: one page-level useData milestones:{full} (TTL.milestones = 30s, collab.js:17), shared entry with the thread sidebar; pagination swaps list windows, not this key, so no storm. Freshness covered: collab invalidation already includes milestones:{full} (collab.js:51).
  9. LAWS — PASS. Law 1: no package.json change. Law 7: no task/SSE surface. Law 8: no registry seams (page-local). Law 12: 12_web_ui.md decision appended, claims verified accurate against the diff.
  10. CHIP LANGUAGE — PASS. Reuses .chip (ui.css:93-94, has dark: variants); A/Show/useData/TTL imports pre-existing, no new imports needed beyond the milestones helper.

VERIFICATION (scratch worktree /tmp/pr386, removed afterward; main untouched, still clean):

  • node --test web/test/unit/*.test.js (node_modules symlinked): 756 pass / 0 fail / 3 skipped with smoke gated off. NOTE: with the default base URL the 2 smoke tests FAIL against whatever already listens on 127.0.0.1:8080 (/ -> 401) — ambient live instance, left untouched, unrelated to this PR; no browser run per review instructions.
  • vite build (./node_modules/.bin/vite build directly; pnpm absent from PATH and installing it is out of scope): exit 0 in 2.09s (only the pre-existing >500kB chunk-size warning).
  • No fixes pushed — nothing to fix.

MERGE RECOMMENDATION: ready to merge (browser proof may follow the shared-daemon loopback-guard convention noted in the doc entry).

REVIEW — PR #386 (fix/issue-380, 3b3229c): 3 files, +125/-2 (Issues.jsx, issues-row-milestone.test.js, 12_web_ui.md decision). No backend change, no new deps, no Go touched (fmt/vet n/a). Acceptance criteria, all verified against the diff: 1. ORDER count -> milestone -> time — PASS. Issues.jsx meta reads count-span, <Show milestone> chip, DateTime; index-order pinned by the new test. 2. UNSET -> no element — PASS. <Show when={issue.milestone != null}>; no placeholder/chip. (== null covers both null and undefined.) 3. PENDING -> placeholder, never raw hex; UNKNOWN -> bare id — PASS. milestoneDisplay(getMilestoneSet()?.milestones, issue.milestone) (milestones.js:29-34); pending renders muted ellipsis placeholder; unknown falls back to bare id — same self-heal stance as labels, consistent. Raw id never renders as text (only inside the filter href). 4. CLICK -> /issues?milestone=<id> — PASS, and slightly better than asked: uses shared milestoneFilterHref (milestones.js:70-72), byte-identical to the Issue.jsx:556 sidebar inline template, also used by Milestones.jsx. Single source, same destination. 5. WORD 'comments' GONE from visible row text — PASS. Old '{count} comments' literal removed; 'comments' survives only inside title/aria-label attrs (explicitly allowed: 'keep an accessible label — implementer's call'). Pulls.jsx rows never had the word (out of scope anyway). NOTE (not blocking): Issue.jsx:406 thread header still renders '{count} comments' — thread page, outside this issue's list-row scope. 6. BUBBLE decorative + label — PASS. aria-hidden bubble span with aria-label + title 'N comments'. Nit (not blocking): aria-label on a role-less span is ignored by some AT, but the count text itself remains exposed, so nothing is lost. 7. TRUNCATION — PASS. chip max-w-40 truncate + full-title tooltip; row keeps min-w-0 flex-wrap context (#334 safety). Tailwind 'truncate' carries overflow/ellipsis/nowrap. 8. FETCH — PASS, as designed: one page-level useData milestones:{full} (TTL.milestones = 30s, collab.js:17), shared entry with the thread sidebar; pagination swaps list windows, not this key, so no storm. Freshness covered: collab invalidation already includes milestones:{full} (collab.js:51). 9. LAWS — PASS. Law 1: no package.json change. Law 7: no task/SSE surface. Law 8: no registry seams (page-local). Law 12: 12_web_ui.md decision appended, claims verified accurate against the diff. 10. CHIP LANGUAGE — PASS. Reuses .chip (ui.css:93-94, has dark: variants); A/Show/useData/TTL imports pre-existing, no new imports needed beyond the milestones helper. VERIFICATION (scratch worktree /tmp/pr386, removed afterward; main untouched, still clean): - node --test web/test/unit/*.test.js (node_modules symlinked): 756 pass / 0 fail / 3 skipped with smoke gated off. NOTE: with the default base URL the 2 smoke tests FAIL against whatever already listens on 127.0.0.1:8080 (/ -> 401) — ambient live instance, left untouched, unrelated to this PR; no browser run per review instructions. - vite build (./node_modules/.bin/vite build directly; pnpm absent from PATH and installing it is out of scope): exit 0 in 2.09s (only the pre-existing >500kB chunk-size warning). - No fixes pushed — nothing to fix. MERGE RECOMMENDATION: ready to merge (browser proof may follow the shared-daemon loopback-guard convention noted in the doc entry).
Author
Owner

Fixed by PR #386 (review clean — all 7 criteria pass, shared href helper, no backend change; 756 pass), merged. Closing.

Fixed by PR #386 (review clean — all 7 criteria pass, shared href helper, no backend change; 756 pass), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#380
No description provided.