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 rusqlite::Connection;
|
||||||
use serde::Serialize;
|
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 {
|
if total == 0 {
|
||||||
return String::new();
|
return StyledCell::plain(String::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
if unresolved > 0 {
|
if unresolved > 0 {
|
||||||
format!("{total}/{unresolved}!")
|
let text = format!("{total}/");
|
||||||
|
let warn = Theme::warning().render(&format!("{unresolved}!"));
|
||||||
|
StyledCell::plain(format!("{text}{warn}"))
|
||||||
} else {
|
} else {
|
||||||
format!("{total}")
|
StyledCell::plain(format!("{total}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +683,8 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Issues (showing {} of {})\n",
|
"{} {} of {}\n",
|
||||||
|
Theme::bold().render("Issues"),
|
||||||
result.issues.len(),
|
result.issues.len(),
|
||||||
result.total_count
|
result.total_count
|
||||||
);
|
);
|
||||||
@@ -698,16 +701,17 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
|
|
||||||
for issue in &result.issues {
|
for issue in &result.issues {
|
||||||
let title = render::truncate(&issue.title, 45);
|
let title = render::truncate(&issue.title, 45);
|
||||||
let relative_time = render::format_relative_time(issue.updated_at);
|
let relative_time = render::format_relative_time_compact(issue.updated_at);
|
||||||
let labels = render::format_labels(&issue.labels, 2);
|
let labels = render::format_labels_bare(&issue.labels, 2);
|
||||||
let assignee = format_assignees(&issue.assignees);
|
let assignee = format_assignees(&issue.assignees);
|
||||||
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
||||||
|
|
||||||
let state_cell = if issue.state == "opened" {
|
let (icon, state_style) = if issue.state == "opened" {
|
||||||
StyledCell::styled(&issue.state, Theme::success())
|
(Icons::issue_opened(), Theme::success())
|
||||||
} else {
|
} 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![
|
let mut row = vec![
|
||||||
StyledCell::styled(format!("#{}", issue.iid), Theme::info()),
|
StyledCell::styled(format!("#{}", issue.iid), Theme::info()),
|
||||||
@@ -730,7 +734,7 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
row.extend([
|
row.extend([
|
||||||
StyledCell::styled(assignee, Theme::accent()),
|
StyledCell::styled(assignee, Theme::accent()),
|
||||||
StyledCell::styled(labels, Theme::warning()),
|
StyledCell::styled(labels, Theme::warning()),
|
||||||
StyledCell::plain(discussions),
|
discussions,
|
||||||
StyledCell::styled(relative_time, Theme::dim()),
|
StyledCell::styled(relative_time, Theme::dim()),
|
||||||
]);
|
]);
|
||||||
table.add_row(row);
|
table.add_row(row);
|
||||||
@@ -783,7 +787,8 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Merge Requests (showing {} of {})\n",
|
"{} {} of {}\n",
|
||||||
|
Theme::bold().render("Merge Requests"),
|
||||||
result.mrs.len(),
|
result.mrs.len(),
|
||||||
result.total_count
|
result.total_count
|
||||||
);
|
);
|
||||||
@@ -796,22 +801,23 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
|
|
||||||
for mr in &result.mrs {
|
for mr in &result.mrs {
|
||||||
let title = if mr.draft {
|
let title = if mr.draft {
|
||||||
format!("[DRAFT] {}", render::truncate(&mr.title, 38))
|
format!("{} {}", Icons::mr_draft(), render::truncate(&mr.title, 42))
|
||||||
} else {
|
} else {
|
||||||
render::truncate(&mr.title, 45)
|
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 branches = format_branches(&mr.target_branch, &mr.source_branch, 25);
|
||||||
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
||||||
|
|
||||||
let state_cell = match mr.state.as_str() {
|
let (icon, style) = match mr.state.as_str() {
|
||||||
"opened" => StyledCell::styled(&mr.state, Theme::success()),
|
"opened" => (Icons::mr_opened(), Theme::success()),
|
||||||
"merged" => StyledCell::styled(&mr.state, Theme::accent()),
|
"merged" => (Icons::mr_merged(), Theme::accent()),
|
||||||
"closed" => StyledCell::styled(&mr.state, Theme::error()),
|
"closed" => (Icons::mr_closed(), Theme::error()),
|
||||||
"locked" => StyledCell::styled(&mr.state, Theme::warning()),
|
"locked" => (Icons::mr_opened(), Theme::warning()),
|
||||||
_ => StyledCell::styled(&mr.state, Theme::dim()),
|
_ => (Icons::mr_opened(), Theme::dim()),
|
||||||
};
|
};
|
||||||
|
let state_cell = StyledCell::styled(format!("{icon} {}", mr.state), style);
|
||||||
|
|
||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
StyledCell::styled(format!("!{}", mr.iid), Theme::info()),
|
StyledCell::styled(format!("!{}", mr.iid), Theme::info()),
|
||||||
@@ -822,7 +828,7 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
Theme::accent(),
|
Theme::accent(),
|
||||||
),
|
),
|
||||||
StyledCell::styled(branches, Theme::info()),
|
StyledCell::styled(branches, Theme::info()),
|
||||||
StyledCell::plain(discussions),
|
discussions,
|
||||||
StyledCell::styled(relative_time, Theme::dim()),
|
StyledCell::styled(relative_time, Theme::dim()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -909,7 +915,8 @@ pub fn print_list_notes(result: &NoteListResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Notes (showing {} of {})\n",
|
"{} {} of {}\n",
|
||||||
|
Theme::bold().render("Notes"),
|
||||||
result.notes.len(),
|
result.notes.len(),
|
||||||
result.total_count
|
result.total_count
|
||||||
);
|
);
|
||||||
@@ -934,7 +941,7 @@ pub fn print_list_notes(result: &NoteListResult) {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let path = format_note_path(note.position_new_path.as_deref(), note.position_new_line);
|
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 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());
|
let note_type = format_note_type(note.note_type.as_deref());
|
||||||
|
|
||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
|
|||||||
@@ -62,17 +62,20 @@ fn format_labels_overflow() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_discussions_empty() {
|
fn format_discussions_empty() {
|
||||||
assert_eq!(format_discussions(0, 0), "");
|
assert_eq!(format_discussions(0, 0).text, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_discussions_no_unresolved() {
|
fn format_discussions_no_unresolved() {
|
||||||
assert_eq!(format_discussions(5, 0), "5");
|
assert_eq!(format_discussions(5, 0).text, "5");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_discussions_with_unresolved() {
|
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