refactor(show): polish issue and MR detail views with section dividers and icons
Phase 4 of the UX overhaul. Restructures the show issue and show MR detail displays with consistent section layout, state icons, and improved typography. Issue detail changes: - Replace bold header + box-drawing underline with indented title using Theme::bold() for the title text only - Organize fields into named sections using render::section_divider(): Details, Development, Description, Discussions - Add state icons (Icons::issue_opened/closed) alongside text labels - Add relative time in parentheses next to Created/Updated dates - Switch labels from "Labels: (none)" to only showing when present, using format_labels_bare for clean comma-separated output - Move URL and confidential indicator into Details section - Closing MRs show state-colored icons (merged/opened/closed) - Discussions use section_divider instead of bold text, remove colons from author lines, adjust wrap widths for consistent indentation MR detail changes: - Same section-divider layout: Details, Description, Discussions - State icons for opened/merged/closed using Icons::mr_* helpers - Draft indicator uses Icons::mr_draft() instead of [Draft] text prefix - Relative times added to Created, Updated, Merged, Closed dates - Reviewers and Assignees fields aligned with fixed-width labels - Labels shown only when present, using format_labels_bare - Discussion formatting matches issue detail style Both views use 5-space left indent for field alignment and consistent wrap widths (72 for descriptions, 68/66 for discussion notes/replies). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
teernisse
parent
4b372dfb38
commit
d0744039ef
@@ -1,4 +1,4 @@
|
|||||||
use crate::cli::render::{self, Theme};
|
use crate::cli::render::{self, Icons, Theme};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@@ -614,33 +614,47 @@ fn wrap_text(text: &str, width: usize, indent: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_show_issue(issue: &IssueDetail) {
|
pub fn print_show_issue(issue: &IssueDetail) {
|
||||||
let header = format!("Issue #{}: {}", issue.iid, issue.title);
|
// Title line
|
||||||
println!("{}", Theme::bold().render(&header));
|
println!(
|
||||||
println!("{}", "\u{2501}".repeat(header.len().min(80)));
|
" Issue #{}: {}",
|
||||||
println!();
|
issue.iid,
|
||||||
|
Theme::bold().render(&issue.title),
|
||||||
|
);
|
||||||
|
|
||||||
println!("Ref: {}", Theme::dim().render(&issue.references_full));
|
// Details section
|
||||||
println!("Project: {}", Theme::info().render(&issue.project_path));
|
println!("{}", render::section_divider("Details"));
|
||||||
|
|
||||||
let state_styled = if issue.state == "opened" {
|
println!(
|
||||||
Theme::success().render(&issue.state)
|
" Ref {}",
|
||||||
|
Theme::muted().render(&issue.references_full)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Project {}",
|
||||||
|
Theme::info().render(&issue.project_path)
|
||||||
|
);
|
||||||
|
|
||||||
|
let (icon, state_style) = if issue.state == "opened" {
|
||||||
|
(Icons::issue_opened(), Theme::success())
|
||||||
} else {
|
} else {
|
||||||
Theme::dim().render(&issue.state)
|
(Icons::issue_closed(), Theme::dim())
|
||||||
};
|
};
|
||||||
println!("State: {}", state_styled);
|
println!(
|
||||||
|
" State {}",
|
||||||
|
state_style.render(&format!("{icon} {}", issue.state))
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(status) = &issue.status_name {
|
||||||
|
println!(
|
||||||
|
" Status {}",
|
||||||
|
render::style_with_hex(status, issue.status_color.as_deref())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if issue.confidential {
|
if issue.confidential {
|
||||||
println!(" {}", Theme::error().bold().render("CONFIDENTIAL"));
|
println!(" {}", Theme::error().bold().render("CONFIDENTIAL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(status) = &issue.status_name {
|
println!(" Author @{}", issue.author_username);
|
||||||
println!(
|
|
||||||
"Status: {}",
|
|
||||||
render::style_with_hex(status, issue.status_color.as_deref())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Author: @{}", issue.author_username);
|
|
||||||
|
|
||||||
if !issue.assignees.is_empty() {
|
if !issue.assignees.is_empty() {
|
||||||
let label = if issue.assignees.len() > 1 {
|
let label = if issue.assignees.len() > 1 {
|
||||||
@@ -649,69 +663,82 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
|||||||
"Assignee"
|
"Assignee"
|
||||||
};
|
};
|
||||||
println!(
|
println!(
|
||||||
"{}:{} {}",
|
" {}{} {}",
|
||||||
label,
|
label,
|
||||||
" ".repeat(10 - label.len()),
|
" ".repeat(12 - label.len()),
|
||||||
issue
|
issue
|
||||||
.assignees
|
.assignees
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| format!("@{}", a))
|
.map(|a| format!("@{a}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Created: {}", format_date(issue.created_at));
|
println!(
|
||||||
println!("Updated: {}", format_date(issue.updated_at));
|
" Created {} ({})",
|
||||||
|
format_date(issue.created_at),
|
||||||
|
render::format_relative_time_compact(issue.created_at),
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Updated {} ({})",
|
||||||
|
format_date(issue.updated_at),
|
||||||
|
render::format_relative_time_compact(issue.updated_at),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(closed_at) = &issue.closed_at {
|
if let Some(closed_at) = &issue.closed_at {
|
||||||
println!("Closed: {}", closed_at);
|
println!(" Closed {closed_at}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(due) = &issue.due_date {
|
if let Some(due) = &issue.due_date {
|
||||||
println!("Due: {}", due);
|
println!(" Due {due}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ms) = &issue.milestone {
|
if let Some(ms) = &issue.milestone {
|
||||||
println!("Milestone: {}", ms);
|
println!(" Milestone {ms}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.labels.is_empty() {
|
if !issue.labels.is_empty() {
|
||||||
println!("Labels: {}", Theme::dim().render("(none)"));
|
println!(
|
||||||
} else {
|
" Labels {}",
|
||||||
println!("Labels: {}", issue.labels.join(", "));
|
render::format_labels_bare(&issue.labels, issue.labels.len())
|
||||||
}
|
);
|
||||||
|
|
||||||
if !issue.closing_merge_requests.is_empty() {
|
|
||||||
println!();
|
|
||||||
println!("{}", Theme::bold().render("Development:"));
|
|
||||||
for mr in &issue.closing_merge_requests {
|
|
||||||
let state_indicator = match mr.state.as_str() {
|
|
||||||
"merged" => Theme::success().render(&mr.state),
|
|
||||||
"opened" => Theme::info().render(&mr.state),
|
|
||||||
"closed" => Theme::error().render(&mr.state),
|
|
||||||
_ => Theme::dim().render(&mr.state),
|
|
||||||
};
|
|
||||||
println!(" !{} {} ({})", mr.iid, mr.title, state_indicator);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(url) = &issue.web_url {
|
if let Some(url) = &issue.web_url {
|
||||||
println!("URL: {}", Theme::dim().render(url));
|
println!(" URL {}", Theme::muted().render(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
// Development section
|
||||||
|
if !issue.closing_merge_requests.is_empty() {
|
||||||
|
println!("{}", render::section_divider("Development"));
|
||||||
|
for mr in &issue.closing_merge_requests {
|
||||||
|
let (mr_icon, mr_style) = match mr.state.as_str() {
|
||||||
|
"merged" => (Icons::mr_merged(), Theme::accent()),
|
||||||
|
"opened" => (Icons::mr_opened(), Theme::success()),
|
||||||
|
"closed" => (Icons::mr_closed(), Theme::error()),
|
||||||
|
_ => (Icons::mr_opened(), Theme::dim()),
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
" {} !{} {} {}",
|
||||||
|
mr_style.render(mr_icon),
|
||||||
|
mr.iid,
|
||||||
|
mr.title,
|
||||||
|
mr_style.render(&mr.state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println!("{}", Theme::bold().render("Description:"));
|
// Description section
|
||||||
|
println!("{}", render::section_divider("Description"));
|
||||||
if let Some(desc) = &issue.description {
|
if let Some(desc) = &issue.description {
|
||||||
let wrapped = wrap_text(desc, 76, " ");
|
let wrapped = wrap_text(desc, 72, " ");
|
||||||
println!(" {}", wrapped);
|
println!(" {wrapped}");
|
||||||
} else {
|
} else {
|
||||||
println!(" {}", Theme::dim().render("(no description)"));
|
println!(" {}", Theme::muted().render("(no description)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
// Discussions section
|
||||||
|
|
||||||
let user_discussions: Vec<&DiscussionDetail> = issue
|
let user_discussions: Vec<&DiscussionDetail> = issue
|
||||||
.discussions
|
.discussions
|
||||||
.iter()
|
.iter()
|
||||||
@@ -719,13 +746,12 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if user_discussions.is_empty() {
|
if user_discussions.is_empty() {
|
||||||
println!("{}", Theme::dim().render("Discussions: (none)"));
|
println!("\n {}", Theme::muted().render("No discussions"));
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
Theme::bold().render(&format!("Discussions ({}):", user_discussions.len()))
|
render::section_divider(&format!("Discussions ({})", user_discussions.len()))
|
||||||
);
|
);
|
||||||
println!();
|
|
||||||
|
|
||||||
for discussion in user_discussions {
|
for discussion in user_discussions {
|
||||||
let user_notes: Vec<&NoteDetail> =
|
let user_notes: Vec<&NoteDetail> =
|
||||||
@@ -733,22 +759,22 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
|||||||
|
|
||||||
if let Some(first_note) = user_notes.first() {
|
if let Some(first_note) = user_notes.first() {
|
||||||
println!(
|
println!(
|
||||||
" {} ({}):",
|
" {} {}",
|
||||||
Theme::info().render(&format!("@{}", first_note.author_username)),
|
Theme::info().render(&format!("@{}", first_note.author_username)),
|
||||||
format_date(first_note.created_at)
|
format_date(first_note.created_at),
|
||||||
);
|
);
|
||||||
let wrapped = wrap_text(&first_note.body, 72, " ");
|
let wrapped = wrap_text(&first_note.body, 68, " ");
|
||||||
println!(" {}", wrapped);
|
println!(" {wrapped}");
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
for reply in user_notes.iter().skip(1) {
|
for reply in user_notes.iter().skip(1) {
|
||||||
println!(
|
println!(
|
||||||
" {} ({}):",
|
" {} {}",
|
||||||
Theme::info().render(&format!("@{}", reply.author_username)),
|
Theme::info().render(&format!("@{}", reply.author_username)),
|
||||||
format_date(reply.created_at)
|
format_date(reply.created_at),
|
||||||
);
|
);
|
||||||
let wrapped = wrap_text(&reply.body, 68, " ");
|
let wrapped = wrap_text(&reply.body, 66, " ");
|
||||||
println!(" {}", wrapped);
|
println!(" {wrapped}");
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,36 +783,49 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_show_mr(mr: &MrDetail) {
|
pub fn print_show_mr(mr: &MrDetail) {
|
||||||
let draft_prefix = if mr.draft { "[Draft] " } else { "" };
|
// Title line
|
||||||
let header = format!("MR !{}: {}{}", mr.iid, draft_prefix, mr.title);
|
let draft_prefix = if mr.draft {
|
||||||
println!("{}", Theme::bold().render(&header));
|
format!("{} ", Icons::mr_draft())
|
||||||
println!("{}", "\u{2501}".repeat(header.len().min(80)));
|
} else {
|
||||||
println!();
|
String::new()
|
||||||
|
|
||||||
println!("Project: {}", Theme::info().render(&mr.project_path));
|
|
||||||
|
|
||||||
let state_styled = match mr.state.as_str() {
|
|
||||||
"opened" => Theme::success().render(&mr.state),
|
|
||||||
"merged" => Theme::accent().render(&mr.state),
|
|
||||||
"closed" => Theme::error().render(&mr.state),
|
|
||||||
_ => Theme::dim().render(&mr.state),
|
|
||||||
};
|
};
|
||||||
println!("State: {}", state_styled);
|
println!(
|
||||||
|
" MR !{}: {}{}",
|
||||||
|
mr.iid,
|
||||||
|
draft_prefix,
|
||||||
|
Theme::bold().render(&mr.title),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Details section
|
||||||
|
println!("{}", render::section_divider("Details"));
|
||||||
|
|
||||||
|
println!(" Project {}", Theme::info().render(&mr.project_path));
|
||||||
|
|
||||||
|
let (icon, state_style) = match mr.state.as_str() {
|
||||||
|
"opened" => (Icons::mr_opened(), Theme::success()),
|
||||||
|
"merged" => (Icons::mr_merged(), Theme::accent()),
|
||||||
|
"closed" => (Icons::mr_closed(), Theme::error()),
|
||||||
|
_ => (Icons::mr_opened(), Theme::dim()),
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
" State {}",
|
||||||
|
state_style.render(&format!("{icon} {}", mr.state))
|
||||||
|
);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Branches: {} -> {}",
|
" Branches {} -> {}",
|
||||||
Theme::info().render(&mr.source_branch),
|
Theme::info().render(&mr.source_branch),
|
||||||
Theme::warning().render(&mr.target_branch)
|
Theme::warning().render(&mr.target_branch)
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("Author: @{}", mr.author_username);
|
println!(" Author @{}", mr.author_username);
|
||||||
|
|
||||||
if !mr.assignees.is_empty() {
|
if !mr.assignees.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"Assignees: {}",
|
" Assignees {}",
|
||||||
mr.assignees
|
mr.assignees
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| format!("@{}", a))
|
.map(|a| format!("@{a}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
);
|
);
|
||||||
@@ -794,48 +833,63 @@ pub fn print_show_mr(mr: &MrDetail) {
|
|||||||
|
|
||||||
if !mr.reviewers.is_empty() {
|
if !mr.reviewers.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"Reviewers: {}",
|
" Reviewers {}",
|
||||||
mr.reviewers
|
mr.reviewers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|r| format!("@{}", r))
|
.map(|r| format!("@{r}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Created: {}", format_date(mr.created_at));
|
println!(
|
||||||
println!("Updated: {}", format_date(mr.updated_at));
|
" Created {} ({})",
|
||||||
|
format_date(mr.created_at),
|
||||||
|
render::format_relative_time_compact(mr.created_at),
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Updated {} ({})",
|
||||||
|
format_date(mr.updated_at),
|
||||||
|
render::format_relative_time_compact(mr.updated_at),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(merged_at) = mr.merged_at {
|
if let Some(merged_at) = mr.merged_at {
|
||||||
println!("Merged: {}", format_date(merged_at));
|
println!(
|
||||||
|
" Merged {} ({})",
|
||||||
|
format_date(merged_at),
|
||||||
|
render::format_relative_time_compact(merged_at),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(closed_at) = mr.closed_at {
|
if let Some(closed_at) = mr.closed_at {
|
||||||
println!("Closed: {}", format_date(closed_at));
|
println!(
|
||||||
|
" Closed {} ({})",
|
||||||
|
format_date(closed_at),
|
||||||
|
render::format_relative_time_compact(closed_at),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if mr.labels.is_empty() {
|
if !mr.labels.is_empty() {
|
||||||
println!("Labels: {}", Theme::dim().render("(none)"));
|
println!(
|
||||||
} else {
|
" Labels {}",
|
||||||
println!("Labels: {}", mr.labels.join(", "));
|
render::format_labels_bare(&mr.labels, mr.labels.len())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(url) = &mr.web_url {
|
if let Some(url) = &mr.web_url {
|
||||||
println!("URL: {}", Theme::dim().render(url));
|
println!(" URL {}", Theme::muted().render(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
// Description section
|
||||||
|
println!("{}", render::section_divider("Description"));
|
||||||
println!("{}", Theme::bold().render("Description:"));
|
|
||||||
if let Some(desc) = &mr.description {
|
if let Some(desc) = &mr.description {
|
||||||
let wrapped = wrap_text(desc, 76, " ");
|
let wrapped = wrap_text(desc, 72, " ");
|
||||||
println!(" {}", wrapped);
|
println!(" {wrapped}");
|
||||||
} else {
|
} else {
|
||||||
println!(" {}", Theme::dim().render("(no description)"));
|
println!(" {}", Theme::muted().render("(no description)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
// Discussions section
|
||||||
|
|
||||||
let user_discussions: Vec<&MrDiscussionDetail> = mr
|
let user_discussions: Vec<&MrDiscussionDetail> = mr
|
||||||
.discussions
|
.discussions
|
||||||
.iter()
|
.iter()
|
||||||
@@ -843,13 +897,12 @@ pub fn print_show_mr(mr: &MrDetail) {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if user_discussions.is_empty() {
|
if user_discussions.is_empty() {
|
||||||
println!("{}", Theme::dim().render("Discussions: (none)"));
|
println!("\n {}", Theme::muted().render("No discussions"));
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
Theme::bold().render(&format!("Discussions ({}):", user_discussions.len()))
|
render::section_divider(&format!("Discussions ({})", user_discussions.len()))
|
||||||
);
|
);
|
||||||
println!();
|
|
||||||
|
|
||||||
for discussion in user_discussions {
|
for discussion in user_discussions {
|
||||||
let user_notes: Vec<&MrNoteDetail> =
|
let user_notes: Vec<&MrNoteDetail> =
|
||||||
@@ -861,22 +914,22 @@ pub fn print_show_mr(mr: &MrDetail) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
" {} ({}):",
|
" {} {}",
|
||||||
Theme::info().render(&format!("@{}", first_note.author_username)),
|
Theme::info().render(&format!("@{}", first_note.author_username)),
|
||||||
format_date(first_note.created_at)
|
format_date(first_note.created_at),
|
||||||
);
|
);
|
||||||
let wrapped = wrap_text(&first_note.body, 72, " ");
|
let wrapped = wrap_text(&first_note.body, 68, " ");
|
||||||
println!(" {}", wrapped);
|
println!(" {wrapped}");
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
for reply in user_notes.iter().skip(1) {
|
for reply in user_notes.iter().skip(1) {
|
||||||
println!(
|
println!(
|
||||||
" {} ({}):",
|
" {} {}",
|
||||||
Theme::info().render(&format!("@{}", reply.author_username)),
|
Theme::info().render(&format!("@{}", reply.author_username)),
|
||||||
format_date(reply.created_at)
|
format_date(reply.created_at),
|
||||||
);
|
);
|
||||||
let wrapped = wrap_text(&reply.body, 68, " ");
|
let wrapped = wrap_text(&reply.body, 66, " ");
|
||||||
println!(" {}", wrapped);
|
println!(" {wrapped}");
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user