refactor(list): polish list commands with icons, compact timestamps, and styled discussions
Phase 3 of the UX overhaul. Enhances the issues, merge requests, and
notes list displays with visual indicators and improved formatting.
List display changes (src/cli/commands/list.rs):
- Add state icons to issues (opened/closed) and merge requests
(opened/merged/closed) using Icons:: helpers alongside text labels
- Replace [DRAFT] prefix with Icons::mr_draft() glyph for draft MRs
- Switch from format_relative_time to format_relative_time_compact for
tighter column widths in tabular output
- Switch from format_labels to format_labels_bare for unlabeled style
- Change format_discussions() return type from String to StyledCell so
unresolved counts render with Theme::warning() color inline
- Bold the section headers ("Issues", "Merge Requests", "Notes")
with count separated from the label for cleaner scanning
- Import Icons from render module
Test updates (src/cli/commands/list_tests.rs):
- Update format_discussions tests to assert on StyledCell.text field
instead of raw String, since the function now returns styled output
- The unresolved-count test checks starts_with/contains to handle
embedded ANSI escape codes from Theme::warning()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
teernisse
parent
af8fc4af76
commit
4b372dfb38
@@ -1,4 +1,4 @@
|
||||
use crate::cli::render::{self, Align, StyledCell, Table as LoreTable, Theme};
|
||||
use crate::cli::render::{self, Align, Icons, StyledCell, Table as LoreTable, Theme};
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -657,15 +657,17 @@ fn format_assignees(assignees: &[String]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_discussions(total: i64, unresolved: i64) -> String {
|
||||
fn format_discussions(total: i64, unresolved: i64) -> StyledCell {
|
||||
if total == 0 {
|
||||
return String::new();
|
||||
return StyledCell::plain(String::new());
|
||||
}
|
||||
|
||||
if unresolved > 0 {
|
||||
format!("{total}/{unresolved}!")
|
||||
let text = format!("{total}/");
|
||||
let warn = Theme::warning().render(&format!("{unresolved}!"));
|
||||
StyledCell::plain(format!("{text}{warn}"))
|
||||
} else {
|
||||
format!("{total}")
|
||||
StyledCell::plain(format!("{total}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,7 +683,8 @@ pub fn print_list_issues(result: &ListResult) {
|
||||
}
|
||||
|
||||
println!(
|
||||
"Issues (showing {} of {})\n",
|
||||
"{} {} of {}\n",
|
||||
Theme::bold().render("Issues"),
|
||||
result.issues.len(),
|
||||
result.total_count
|
||||
);
|
||||
@@ -698,16 +701,17 @@ pub fn print_list_issues(result: &ListResult) {
|
||||
|
||||
for issue in &result.issues {
|
||||
let title = render::truncate(&issue.title, 45);
|
||||
let relative_time = render::format_relative_time(issue.updated_at);
|
||||
let labels = render::format_labels(&issue.labels, 2);
|
||||
let relative_time = render::format_relative_time_compact(issue.updated_at);
|
||||
let labels = render::format_labels_bare(&issue.labels, 2);
|
||||
let assignee = format_assignees(&issue.assignees);
|
||||
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
||||
|
||||
let state_cell = if issue.state == "opened" {
|
||||
StyledCell::styled(&issue.state, Theme::success())
|
||||
let (icon, state_style) = if issue.state == "opened" {
|
||||
(Icons::issue_opened(), Theme::success())
|
||||
} else {
|
||||
StyledCell::styled(&issue.state, Theme::dim())
|
||||
(Icons::issue_closed(), Theme::dim())
|
||||
};
|
||||
let state_cell = StyledCell::styled(format!("{icon} {}", issue.state), state_style);
|
||||
|
||||
let mut row = vec![
|
||||
StyledCell::styled(format!("#{}", issue.iid), Theme::info()),
|
||||
@@ -730,7 +734,7 @@ pub fn print_list_issues(result: &ListResult) {
|
||||
row.extend([
|
||||
StyledCell::styled(assignee, Theme::accent()),
|
||||
StyledCell::styled(labels, Theme::warning()),
|
||||
StyledCell::plain(discussions),
|
||||
discussions,
|
||||
StyledCell::styled(relative_time, Theme::dim()),
|
||||
]);
|
||||
table.add_row(row);
|
||||
@@ -783,7 +787,8 @@ pub fn print_list_mrs(result: &MrListResult) {
|
||||
}
|
||||
|
||||
println!(
|
||||
"Merge Requests (showing {} of {})\n",
|
||||
"{} {} of {}\n",
|
||||
Theme::bold().render("Merge Requests"),
|
||||
result.mrs.len(),
|
||||
result.total_count
|
||||
);
|
||||
@@ -796,22 +801,23 @@ pub fn print_list_mrs(result: &MrListResult) {
|
||||
|
||||
for mr in &result.mrs {
|
||||
let title = if mr.draft {
|
||||
format!("[DRAFT] {}", render::truncate(&mr.title, 38))
|
||||
format!("{} {}", Icons::mr_draft(), render::truncate(&mr.title, 42))
|
||||
} else {
|
||||
render::truncate(&mr.title, 45)
|
||||
};
|
||||
|
||||
let relative_time = render::format_relative_time(mr.updated_at);
|
||||
let relative_time = render::format_relative_time_compact(mr.updated_at);
|
||||
let branches = format_branches(&mr.target_branch, &mr.source_branch, 25);
|
||||
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
||||
|
||||
let state_cell = match mr.state.as_str() {
|
||||
"opened" => StyledCell::styled(&mr.state, Theme::success()),
|
||||
"merged" => StyledCell::styled(&mr.state, Theme::accent()),
|
||||
"closed" => StyledCell::styled(&mr.state, Theme::error()),
|
||||
"locked" => StyledCell::styled(&mr.state, Theme::warning()),
|
||||
_ => StyledCell::styled(&mr.state, Theme::dim()),
|
||||
let (icon, style) = match mr.state.as_str() {
|
||||
"opened" => (Icons::mr_opened(), Theme::success()),
|
||||
"merged" => (Icons::mr_merged(), Theme::accent()),
|
||||
"closed" => (Icons::mr_closed(), Theme::error()),
|
||||
"locked" => (Icons::mr_opened(), Theme::warning()),
|
||||
_ => (Icons::mr_opened(), Theme::dim()),
|
||||
};
|
||||
let state_cell = StyledCell::styled(format!("{icon} {}", mr.state), style);
|
||||
|
||||
table.add_row(vec![
|
||||
StyledCell::styled(format!("!{}", mr.iid), Theme::info()),
|
||||
@@ -822,7 +828,7 @@ pub fn print_list_mrs(result: &MrListResult) {
|
||||
Theme::accent(),
|
||||
),
|
||||
StyledCell::styled(branches, Theme::info()),
|
||||
StyledCell::plain(discussions),
|
||||
discussions,
|
||||
StyledCell::styled(relative_time, Theme::dim()),
|
||||
]);
|
||||
}
|
||||
@@ -909,7 +915,8 @@ pub fn print_list_notes(result: &NoteListResult) {
|
||||
}
|
||||
|
||||
println!(
|
||||
"Notes (showing {} of {})\n",
|
||||
"{} {} of {}\n",
|
||||
Theme::bold().render("Notes"),
|
||||
result.notes.len(),
|
||||
result.total_count
|
||||
);
|
||||
@@ -934,7 +941,7 @@ pub fn print_list_notes(result: &NoteListResult) {
|
||||
.unwrap_or_default();
|
||||
let path = format_note_path(note.position_new_path.as_deref(), note.position_new_line);
|
||||
let parent = format_note_parent(note.noteable_type.as_deref(), note.parent_iid);
|
||||
let relative_time = render::format_relative_time(note.created_at);
|
||||
let relative_time = render::format_relative_time_compact(note.created_at);
|
||||
let note_type = format_note_type(note.note_type.as_deref());
|
||||
|
||||
table.add_row(vec![
|
||||
|
||||
@@ -62,17 +62,20 @@ fn format_labels_overflow() {
|
||||
|
||||
#[test]
|
||||
fn format_discussions_empty() {
|
||||
assert_eq!(format_discussions(0, 0), "");
|
||||
assert_eq!(format_discussions(0, 0).text, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_discussions_no_unresolved() {
|
||||
assert_eq!(format_discussions(5, 0), "5");
|
||||
assert_eq!(format_discussions(5, 0).text, "5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_discussions_with_unresolved() {
|
||||
assert_eq!(format_discussions(5, 2), "5/2!");
|
||||
let cell = format_discussions(5, 2);
|
||||
// Text contains styled ANSI for warning-colored unresolved count
|
||||
assert!(cell.text.starts_with("5/"), "got: {}", cell.text);
|
||||
assert!(cell.text.contains("2!"), "got: {}", cell.text);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user