git-ents.gitmain
⌘K
foforge
commit 92292a2
roots: title-case page and form headings and unify empty states

Static route titles, form headings, and button labels get real Title-Case; tab labels, card headers, badges, and the meta rail keep their lowercase mono chrome. A shared pages::blankslate helper replaces the header-only effects table, the bare toolchains and inbox rules, and the plain no-issues text; the search page distinguishes an untyped query (a type-to-search prompt naming the header’s search input) from a query with no matches; and the files root drops its self-referencing crumb.

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/tests/router.rs @@ -1761,7 +1761,7 @@ .to_vec(), ) .expect("utf8 html"); - assert!(first_body.contains("conversation")); + assert!(first_body.contains("Conversation")); assert!(first_body.contains("left at the first commit")); assert!(first_body.contains("commenter")); assert!( @@ -1837,7 +1837,7 @@ // The commit page renders the review's verdict, body, and reviewer. let page = get_body(&router, &format!("/commit/{oid}")).await; - assert!(page.contains("reviews"), "the reviews section renders"); + assert!(page.contains("Reviews"), "the reviews section renders"); assert!( page.contains("class=\"verdict\""), "the verdict renders prominently" @@ -1982,7 +1982,7 @@ let detail = get_body(&router, &format!("/issues/{id}")).await; assert!(detail.contains("gate rejects a valid signature")); assert!(detail.contains("the full body")); - assert!(detail.contains("discussion")); + assert!(detail.contains("Discussion")); // A comment naming the issue as its context joins the thread. let (cookie, csrf) = session_cookie_and_csrf(&router, &state, "/issues").await; @@ -2098,8 +2098,9 @@ assert!(body.contains("/files/src/needle.rs")); } -/// `GET /search` with no query renders a friendly blankslate rather than -/// an empty or error page. +/// `GET /search` with no query renders a "type to search" blankslate +/// naming the header's own search input -- not a "no matches" one, since +/// nothing was searched yet -- rather than an empty or error page. #[tokio::test] async fn search_with_no_query_renders_a_blankslate() { let state = build_state(FixtureIdentity { @@ -2124,7 +2125,11 @@ .expect("body") .to_bytes(); let body = String::from_utf8(body.to_vec()).expect("utf8 html"); - assert!(body.contains("No matches")); + assert!(body.contains("Type to search")); + assert!( + body.contains("Jump to file or symbol"), + "the prompt names the header's own search input" + ); } /// `GET /toolchains` lists a toolchain written by an older schema (piece
crates/cli/ents-web/src/pages/account.rs @@ -55,16 +55,16 @@ &super::RepoHeader::from_state(&state), &super::identity_label(&state), super::Tab::Account, - "account", + "Account", html! { div.readable { (view) - h2 { "create or update" } + h2 { "Create or Update" } form method="post" action="/account" { (super::csrf_input(&session)) label { "member" input type="text" name="member" value=(member_value); } label { "login" input type="text" name="login" value=(login_value); } - button type="submit" { "save" } + button type="submit" { "Save" } } } },
crates/cli/ents-web/src/pages/comments.rs @@ -83,18 +83,25 @@ &super::RepoHeader::from_state(&state), &super::identity_label(&state), super::Tab::Comments, - "comments", + "Comments", html! { div.readable { - ul { - @for (id, comment) in &rows { - li { - a href=(format!("/comments/{id}")) { (ents_forge::abbreviate_id(id)) } - ": " (comment.body) + @if rows.is_empty() { + (super::blankslate( + "No comments yet", + 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) + } } } } - h2 { "add a comment" } + h2 { "Add a Comment" } (add_form(&query.rev, &session, &query.file, &query.lines)) } }, @@ -148,7 +155,7 @@ &super::RepoHeader::from_state(&state), &super::identity_label(&state), super::Tab::Comments, - ents_forge::abbreviate_id(&id), + &format!("Comment {}", ents_forge::abbreviate_id(&id)), html! { (super::child_crumbs("comments", "/comments", ents_forge::abbreviate_id(&id))) div.readable { @@ -326,19 +333,19 @@ (super::csrf_input(session)) input type="hidden" name="return_to" value=(return_to); label { "reply" textarea name="body" {} } - button type="submit" { "reply" } + button type="submit" { "Reply" } } @if resolved { form method="post" action=(format!("/comments/{id}/reopen")) { (super::csrf_input(session)) input type="hidden" name="return_to" value=(return_to); - button type="submit" { "reopen" } + button type="submit" { "Reopen" } } } @else { form method="post" action=(format!("/comments/{id}/resolve")) { (super::csrf_input(session)) input type="hidden" name="return_to" value=(return_to); - button type="submit" { "resolve" } + button type="submit" { "Resolve" } } } } @@ -423,7 +430,7 @@ label { "rev" input type="text" name="rev" value=(default_rev); } label { "lines" input type="text" name="lines" value=(prefill_lines); } label { "body" textarea name="body" {} } - button type="submit" { "comment" } + button type="submit" { "Comment" } } } }
crates/cli/ents-web/src/pages/commits.rs @@ -94,7 +94,7 @@ &super::RepoHeader::from_state(&state), &super::identity_label(&state), super::Tab::Commits, - "commits", + "Commits", html! { @if rows.is_empty() { (blankslate()) @@ -188,17 +188,13 @@ (rows, older) } -/// The empty-history placeholder: an unborn `HEAD`, or a repository this -/// page could not open at all. +/// The empty-history placeholder ([`super::blankslate`]): an unborn +/// `HEAD`, or a repository this page could not open at all. fn blankslate() -> Markup { - html! { - div.card { - div.blankslate { - h2 { "No commits yet" } - p { "This repository has no history to show." } - } - } - } + super::blankslate( + "No commits yet", + html! { "This repository has no history to show." }, + ) } /// `GET /commit/{oid}`: a single commit's full message, metadata, and a @@ -303,7 +299,7 @@ } @if !comments.is_empty() { div.readable { - h2 { "conversation" } + h2 { "Conversation" } @for (index, comment) in comments.iter().enumerate() { (super::comments::comment_card(index, comment, super::comments::LinkMode::CrossFile)) } @@ -339,7 +335,7 @@ .unwrap_or_default(); let return_to = format!("/commit/{oid}"); html! { - h2 { "reviews" } + h2 { "Reviews" } @for ((target, member), review) in &reviews { div.card { div.comment-meta { @@ -384,7 +380,7 @@ (super::csrf_input(session)) input type="hidden" name="return_to" value=(return_to); label { "comment on this review" textarea name="body" {} } - button type="submit" { "comment" } + button type="submit" { "Comment" } } } } @@ -468,7 +464,7 @@ option value="request-changes" {} } label { "body" textarea name="body" {} } - button type="submit" { "start a review" } + button type="submit" { "Start a Review" } } } }
crates/cli/ents-web/src/pages/dashboard.rs @@ -52,7 +52,7 @@ &super::RepoHeader::from_state(&state), &super::identity_label(&state), super::Tab::Overview, - "overview", + "Overview", html! { div.overview { div { (main) } @@ -171,17 +171,13 @@ } } -/// The empty-column placeholder shown when the repository has no `README`, -/// no readable root, or no `HEAD` at all. +/// The empty-column placeholder ([`super::blankslate`]) shown when the +/// repository has no `README`, no readable root, or no `HEAD` at all. fn blankslate() -> Markup { - html! { - div.card { - div.blankslate { - h2 { "Nothing to show yet" } - p { "Add a " code { "README" } " or browse the repository in " a href="/files" { "Files" } "." } - } - } - } + super::blankslate( + "Nothing to show yet", + html! { "Add a " code { "README" } " or browse the repository in " a href="/files" { "Files" } "." }, + ) } /// The first root-tree blob whose stem is `README` and whose extension
crates/cli/ents-web/src/pages/effects.rs @@ -27,12 +27,20 @@ O: Find + Write + Send + 'static, { let rows = read_all(&state)?; + let body = if rows.is_empty() { + super::blankslate( + "No effects yet", + html! { "Registered effects and their trigger queries appear here." }, + ) + } else { + crate::render::list_table(&rows, "name", |id| format!("/effects/{id}")) + }; Ok(super::layout_meta( &super::RepoHeader::from_state(&state), &super::identity_label(&state), "/effects", - "effects", - crate::render::list_table(&rows, "name", |id| format!("/effects/{id}")), + "Effects", + body, )) }
crates/cli/ents-web/src/pages/files.rs @@ -123,9 +123,8 @@ &super::RepoHeader::from_state(state), &super::identity_label(state), super::Tab::Files, - "files", + "Files", html! { - (crumbs(path)) (dir_listing(path, Vec::new())) }, )); @@ -143,9 +142,8 @@ &super::RepoHeader::from_state(state), &super::identity_label(state), super::Tab::Files, - "files", + "Files", html! { - (crumbs(path)) (dir_listing(path, entries)) }, )); @@ -283,7 +281,10 @@ /// `path`, `chevron-right` icons separating segments -- pure navigation, /// no trailing actions. The history/comment links that used to trail this /// nav on a blob view now live in [`blob_header`]'s own action group -/// instead (see this module's own top-level doc for why). +/// instead (see this module's own top-level doc for why). The files root +/// itself renders no crumbs at all: a lone self-referencing "files" crumb +/// under the page's own "Files" title (and above the listing card's own +/// "files" header) named the same place three times. fn crumbs(path: &str) -> Markup { let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); let mut acc = String::new(); @@ -652,7 +653,7 @@ return html! {}; } html! { - h2 { "outdated comments" } + h2 { "Outdated Comments" } @for &(index, comment) in comments { (super::comments::comment_card(index, comment, super::comments::LinkMode::SameFile)) }
crates/cli/ents-web/src/pages/inbox.rs @@ -29,11 +29,19 @@ rows.push(rest.to_owned()); } } + let body = if rows.is_empty() { + super::blankslate( + "Inbox is empty", + maud::html! { "Entries awaiting adoption appear here." }, + ) + } else { + crate::render::string_list(&rows, |_| "/inbox".to_owned()) + }; Ok(super::layout_meta( &super::RepoHeader::from_state(&state), &super::identity_label(&state), "/inbox", - "inbox", - crate::render::string_list(&rows, |_| "/inbox".to_owned()), + "Inbox", + body, )) }
crates/cli/ents-web/src/pages/issues.rs @@ -49,11 +49,14 @@ &super::RepoHeader::from_state(&state), &super::identity_label(&state), super::Tab::Issues, - "issues", + "Issues", html! { div.readable { @if rows.is_empty() { - p { "No issues yet." } + (super::blankslate( + "No issues yet", + html! { "Open one with the form below." }, + )) } @else { table.entity-list { thead { @@ -71,7 +74,7 @@ } } } - h2 { "open an issue" } + h2 { "Open an Issue" } (new_form(&session)) } }, @@ -121,12 +124,12 @@ div.doc-body { (body) } } details { - summary { "edit" } + summary { "Edit" } (edit_form(&session, &issue)) } - h2 { "discussion" } + h2 { "Discussion" } (crate::pages::comments::thread_section(&state, &session, &thread, &return_to)) - h2 { "add a comment" } + h2 { "Add a Comment" } (comment_form(&session, &id)) } }, @@ -326,7 +329,7 @@ label { "assignees" input type="text" name="assignees" placeholder="alice, bob"; } label { "labels" input type="text" name="labels" placeholder="bug, gate"; } label { "body" textarea name="body" {} } - button type="submit" { "open issue" } + button type="submit" { "Open Issue" } } } } @@ -348,7 +351,7 @@ input type="text" name="assignees" value=(join_members(&issue.assignees)); } label { "labels" input type="text" name="labels" value=(issue.labels.join(", ")); } - button type="submit" { "save" } + button type="submit" { "Save" } } } } @@ -373,7 +376,7 @@ form method="post" action=(format!("/issues/{id}/comment")) { (super::csrf_input(session)) label { "body" textarea name="body" {} } - button type="submit" { "comment" } + button type="submit" { "Comment" } } } }
crates/cli/ents-web/src/pages/members.rs @@ -23,12 +23,20 @@ O: Find + Write + Send + 'static, { let rows = read_all(&state)?; + let body = if rows.is_empty() { + super::blankslate( + "No members yet", + maud::html! { "Enroll one with " code { "git ents members add" } "." }, + ) + } else { + crate::render::list_table(&rows, "username", |id| format!("/members/{id}")) + }; Ok(super::layout_meta( &super::RepoHeader::from_state(&state), &super::identity_label(&state), "/members", - "members", - crate::render::list_table(&rows, "username", |id| format!("/members/{id}")), + "Members", + body, )) }
crates/cli/ents-web/src/pages/meta.rs @@ -21,7 +21,7 @@ &super::RepoHeader::from_state(&state), &super::identity_label(&state), super::Tab::Meta, - "meta", + "Meta", html! { div.card { div.card-header { "meta" }
crates/cli/ents-web/src/pages/mod.rs @@ -390,6 +390,23 @@ } } +/// The shared empty-state card (`ents.css`'s `.blankslate`): a short +/// title and one explanatory line, rendered instead of a bare list or a +/// header-only table when a page family has nothing to show yet. `line` +/// is markup, not text, so a page can point at its own create form or +/// link a next step ([`super::dashboard`]'s README pointer does the +/// same). +pub(crate) fn blankslate(title: &str, line: Markup) -> Markup { + html! { + div.card { + div.blankslate { + h2 { (title) } + p { (line) } + } + } + } +} + /// The one-level breadcrumb trail every `/{id}` child page renders above /// its own content -- "parent \u{203a} here", reusing the `.crumbs` markup /// pattern [`super::files`]'s own multi-level path trail already renders
crates/cli/ents-web/src/pages/redactions.rs @@ -22,12 +22,20 @@ O: Find + Write + Send + 'static, { let rows = read_all(&state)?; + let body = if rows.is_empty() { + super::blankslate( + "No redactions yet", + maud::html! { "Record one with " code { "git ents redact add" } "." }, + ) + } else { + crate::render::list_table(&rows, "id", |id| format!("/redactions/{id}")) + }; Ok(super::layout_meta( &super::RepoHeader::from_state(&state), &super::identity_label(&state), "/redactions", - "redactions", - crate::render::list_table(&rows, "id", |id| format!("/redactions/{id}")), + "Redactions", + body, )) }
crates/cli/ents-web/src/pages/search.rs @@ -28,8 +28,9 @@ #[derive(Debug, Deserialize)] pub struct SearchQuery { /// The search term. Empty (the default, and what a bare `GET /search` - /// carries) renders the same friendly blankslate as a query with no - /// matches. + /// carries) renders a "type to search" blankslate rather than a + /// no-matches one -- nothing was searched yet, so nothing "failed to + /// match" (see [`blankslate`]'s own doc). #[serde(default)] q: String, } @@ -63,7 +64,7 @@ &super::RepoHeader::from_state(&state), &super::identity_label(&state), super::Tab::None, - "search", + "Search", html! { @if !any_matches { (blankslate(&query)) @@ -223,20 +224,25 @@ } } -/// The empty-results placeholder, shown for an empty query and for a -/// non-empty one with no matches at all. +/// The empty-results placeholder ([`super::blankslate`]): a "type to +/// search" prompt before any query has been typed at all (naming the +/// header's own "Jump to file or symbol" search input, this page's only +/// entry point), or a "no matches" note for a non-empty query that found +/// nothing. fn blankslate(query: &str) -> Markup { - html! { - div.card { - div.blankslate { - h2 { "No matches" } - @if query.is_empty() { - p { "Type a search term above to look through files and meta entities." } - } @else { - p { "Nothing matched " code { (query) } "." } - } - } - } + if query.is_empty() { + super::blankslate( + "Type to search", + html! { + "Use the header's \u{201c}Jump to file or symbol\u{201d} search " + "to look through files and meta entities." + }, + ) + } else { + super::blankslate( + "No matches", + html! { "Nothing matched " code { (query) } "." }, + ) } }
crates/cli/ents-web/src/pages/toolchains.rs @@ -47,11 +47,12 @@ (name, detail) }) .collect(); - Ok(super::layout_meta( - &super::RepoHeader::from_state(&state), - &super::identity_label(&state), - "/toolchains", - "toolchains", + let body = if rows.is_empty() { + super::blankslate( + "No toolchains yet", + html! { "Import one with " code { "git ents toolchain import" } "." }, + ) + } else { html! { div.card { ul.string-list { @@ -65,7 +66,14 @@ } } } - }, + } + }; + Ok(super::layout_meta( + &super::RepoHeader::from_state(&state), + &super::identity_label(&state), + "/toolchains", + "Toolchains", + body, )) } @@ -106,7 +114,7 @@ dt { "name" } dd { (toolchain.name) } dt { "recipe" } dd { (format!("{recipe:?}")) } } - h2 { "import log" } + h2 { "Import Log" } ul { @for oid in &log { li { (oid.to_string()) }