git-ents.gitmain
⌘K
foforge
commit 4803e4d
roots: card the comment pages and humanize anchor locators

The comment page rendered its body as a bare dl row and its anchor through Debug formatting (Some(LineRange { .. })); the threads listing was a plain ul of raw bodies. Both now render the same card markup as every other comment surface: state badge, context and anchor locators as links (path:21-23, jumping into the file browser’s gutter), a human sentence for the projection result, and the body as AsciiDoc. The commit page’s Reviews section gains an empty-state line pointing at the verdict form below it.

Assisted-by: Claude:claude-fable-5

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/cli/ents-web/src/assets/ents.css @@ -196,17 +196,17 @@ .entry-size { margin-left: auto; font-family: var(--font-mono); font-size: .82rem; color: var(--color-text-muted); white-space: nowrap; } /* The workbench dashboard (`GET /`, `crate::pages::dashboard`): three - * cards on a `.desk` grid (working tree, needs attention, tickets), then + * cards on a `.desk` grid (working tree, needs attention, issues), then * a full-width `.desk-wide` History card. `.content` already pads the * column, so the desk itself only spaces its cards. */ .desk { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; align-items: start; margin-bottom: 1.25rem; } .desk .card, .desk-wide .card { margin-bottom: 0; } .card-header .btn-ghost { margin-left: auto; } -.card-row.muted { color: var(--color-text-muted); } +.muted { color: var(--color-text-muted); } .btn { display: inline-block; font-size: .78rem; font-weight: 600; color: var(--color-bg); background: var(--color-accent); border: none; border-radius: 8px; padding: .3rem .8rem; cursor: pointer; text-decoration: none; } .btn-ghost { color: var(--color-accent); background: transparent; border: 1px solid var(--color-border); text-transform: none; letter-spacing: 0; } .btn-ghost:hover { border-color: var(--color-accent); color: var(--color-accent); } -/* Needs-attention / ticket rows: a block link pairing a `.what` line with +/* Needs-attention / issue rows: a block link pairing a `.what` line with * a muted, mono `.where` locator. */ .attention-row { display: block; padding: .7rem 1.1rem; color: inherit; text-decoration: none; } .attention-row:hover { background: var(--color-code-bg); text-decoration: none; }
crates/cli/ents-web/src/pages/comments.rs @@ -97,13 +97,8 @@ html! { "Anchor one to a file with the form below." }, )) } @else { - ul { - @for (id, comment) in &rows { - li { - a href=(format!("/comments/{id}")) { (ents_forge::abbreviate_id(id)) } - ": " (comment.body) - } - } + @for (id, comment) in &rows { + (listing_card(&state, id, comment)) } } h2 { "Add a Comment" } @@ -177,6 +172,8 @@ }; let resolved = comment.state == "resolved"; let return_to = format!("/comments/{id}"); + let body = + crate::asciidoc::to_html(&comment.body).unwrap_or_else(|_| html! { p { (comment.body) } }); Ok(super::layout( &super::RepoHeader::from_state(&state), &super::identity_label(&state), @@ -185,20 +182,29 @@ html! { (super::child_crumbs("comments", "/comments", ents_forge::abbreviate_id(&id))) div.readable { - dl { - dt { "state" } dd { (comment.state) } - @if let Some(context) = &comment.context { - dt { "context" } dd { (context) } + div.card { + div.comment-meta { + span.comment-state { (comment.state) } + @if let Some(context) = &comment.context { + (context_link(context)) + } + @if let Some(parent) = &comment.parent { + a href={ "/comments/" (parent) } { + "in reply to " (ents_forge::abbreviate_id(parent)) + } + } + @if let Some((anchor, _)) = &projected { + a href={ "/files/" (anchor.path) (line_fragment(anchor.lines)) } { + (anchor.path) (line_label(anchor.lines)) + } + } } - @if let Some(parent) = &comment.parent { - dt { "parent" } dd { (parent) } + @if let Some((_, projection)) = &projected { + div.comment-meta { + span { "at " (query.rev) ": " (projection_label(projection)) } + } } - @if let Some((anchor, projection)) = &projected { - dt { "path" } dd { (anchor.path) } - dt { "lines" } dd { (format!("{:?}", anchor.lines)) } - dt { "projection at " (query.rev) } dd { (format!("{projection:?}")) } - } - dt { "body" } dd { (comment.body) } + div.doc-body { (body) } } (action_forms(&session, &id, resolved, &return_to)) } @@ -206,6 +212,100 @@ )) } +/// The `#L<start>[-L<end>]` fragment a files link carries for an anchored +/// range, or nothing for a whole-file anchor -- the same fragment shape +/// `crate::pages::files`'s gutter anchors and `ents.js`'s hash handling +/// use. +fn line_fragment(lines: Option<LineRange>) -> String { + match lines { + Some(range) if range.start == range.end => format!("#L{}", range.start), + Some(range) => format!("#L{}-L{}", range.start, range.end), + None => String::new(), + } +} + +/// The `:21` / `:21-23` suffix a path locator shows for an anchored range, +/// or nothing for a whole-file anchor. +fn line_label(lines: Option<LineRange>) -> String { + match lines { + Some(range) if range.start == range.end => format!(":{}", range.start), + Some(range) => format!(":{}-{}", range.start, range.end), + None => String::new(), + } +} + +/// One human sentence for a projection result (`anchor.projection`) -- +/// never the enum's `Debug` form. +fn projection_label(projection: &Projection) -> String { + match projection { + Projection::Current => "anchored lines unchanged".to_owned(), + Projection::Relocated { path, lines } => { + format!("moved to {path}{}", line_label(*lines)) + } + Projection::Outdated { path } => { + format!("outdated \u{2014} the anchored lines in {path} have been edited") + } + Projection::Deleted => "the anchored file no longer exists".to_owned(), + } +} + +/// A context's own page link: `issues/<id>` and `reviews/<target>/<member>` +/// land on the issue and commit pages that render those threads +/// (`model.comment-context`); any other context renders as plain text. +fn context_link(context: &str) -> Markup { + if let Some(id) = context.strip_prefix("issues/") { + html! { a href={ "/issues/" (id) } { "on issue " (ents_forge::abbreviate_id(id)) } } + } else if let Some(rest) = context.strip_prefix("reviews/") { + let target = rest.split('/').next().unwrap_or(rest); + html! { a href={ "/commit/" (target) } { "on a review of " (ents_forge::abbreviate_id(target)) } } + } else { + html! { span { (context) } } + } +} + +/// One `GET /comments` row: the comment's own card -- an abbreviated-id +/// link to its page, author and age off its ref's tip commit (best +/// effort, like [`thread_comment_card`]'s), its state badge, its context +/// or anchor locator, and its body rendered as AsciiDoc like every other +/// comment card in this crate. +fn listing_card<O: Find + Write>( + state: &AppState<O>, + id: &str, + comment: &comment::Comment, +) -> Markup { + let authorship = ents_model::namespace::comment_ref(id) + .ok() + .and_then(|ref_name| state.refs.get(ref_name.as_ref()).ok().flatten()) + .and_then(|tip| super::commit_authorship(&*state.objects(), tip).ok()); + let anchor = comment + .anchor + .as_ref() + .and_then(|raw| facet_git_tree::deserialize::<Anchor>(&raw.oid(), &*state.objects()).ok()); + let body = + crate::asciidoc::to_html(&comment.body).unwrap_or_else(|_| html! { p { (comment.body) } }); + html! { + div.card { + div.comment-meta { + a href={ "/comments/" (id) } { (ents_forge::abbreviate_id(id)) } + @if let Some((author, seconds)) = &authorship { + span.author { (author) } + span { (super::ago(*seconds)) } + } + span.comment-state { (comment.state) } + @if let Some(context) = &comment.context { + (context_link(context)) + } + @if let Some(anchor) = &anchor { + a href={ "/files/" (anchor.path) (line_fragment(anchor.lines)) } { + (anchor.path) (line_label(anchor.lines)) + } + } + } + div.doc-body { (body) } + } + } +} + /// The form fields the reply route accepts. #[derive(Debug, Deserialize)] pub struct ReplyForm {
crates/cli/ents-web/src/pages/commits.rs @@ -365,6 +365,9 @@ let return_to = format!("/commit/{oid}"); html! { h2 { "Reviews" } + @if reviews.is_empty() { + p.muted { "No reviews of this commit yet \u{2014} record a verdict below." } + } @for ((target, member), review) in &reviews { div.card { div.comment-meta {