use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use super::{badge, empty, field, heading, loading, short_date, state_color, type_color, wrap};
use crate::api::{WorkItem, WorkItemDetails};
use crate::app::{App, DetailTab};
pub fn lines(
app: &App,
item: &WorkItem,
details: Option<&WorkItemDetails>,
tab: DetailTab,
width: u16,
) -> Vec<Line<'static>> {
match tab {
DetailTab::Overview => overview(app, item, details, width),
DetailTab::Children => {
details.map_or_else(|| loading(app), |details| children(app, details, width))
}
_ => details.map_or_else(|| loading(app), |details| comments(app, details, width)),
}
}
fn overview(
app: &App,
item: &WorkItem,
details: Option<&WorkItemDetails>,
width: u16,
) -> Vec<Line<'static>> {
let muted = Style::new().fg(app.theme.muted);
let fields = &item.fields;
let state = details.map_or(&fields.state, |d| &d.state);
let mut lines = vec![
field(
app,
"Type",
vec![Span::styled(
fields.work_item_type.clone(),
Style::new()
.fg(type_color(app, &fields.work_item_type))
.add_modifier(Modifier::BOLD),
)],
),
field(app, "State", vec![badge(state, state_color(app, state))]),
];
let Some(details) = details else {
lines.push(Line::default());
lines.extend(loading(app));
return lines;
};
if let Some(column) = &details.board_column {
let mut value = vec![Span::raw(column.clone())];
if details.board_column_done {
value.push(Span::styled(" · Done", muted));
}
lines.push(field(app, "Column", value));
}
if let Some(priority) = fields.priority {
lines.push(field(
app,
"Priority",
vec![Span::raw(priority.to_string())],
));
}
lines.push(field(
app,
"Assigned to",
vec![match &details.assigned_to {
Some(name) => Span::raw(name.clone()),
None => Span::styled("unassigned", muted),
}],
));
if let Some(iteration) = &details.iteration_path {
lines.push(field(app, "Sprint", vec![Span::raw(iteration.clone())]));
}
if let Some(tags) = details.tags.as_deref().filter(|t| !t.is_empty()) {
let tags = tags
.split(';')
.map(str::trim)
.filter(|t| !t.is_empty())
.flat_map(|tag| {
[
Span::styled(
format!(" {tag} "),
Style::new()
.fg(app.theme.selection_fg)
.bg(app.theme.selection_bg),
),
Span::raw(" "),
]
})
.collect();
lines.push(field(app, "Tags", tags));
}
for (title, text) in [
("Description", &details.description),
("Repro steps", &details.repro_steps),
("Acceptance criteria", &details.acceptance_criteria),
] {
if let Some(text) = text.as_deref().filter(|t| !t.is_empty()) {
lines.push(Line::default());
lines.push(heading(app, title, width));
lines.extend(wrap(text, width, Span::raw(" ")));
}
}
lines
}
fn children(app: &App, details: &WorkItemDetails, width: u16) -> Vec<Line<'static>> {
let muted = Style::new().fg(app.theme.muted);
if details.children.is_empty() {
return empty(app, "No children");
}
let mut types: Vec<&str> = details
.children
.iter()
.map(|child| child.fields.work_item_type.as_str())
.collect();
types.sort();
types.dedup();
let mut lines = Vec::new();
for kind in types {
let children: Vec<_> = details
.children
.iter()
.filter(|child| child.fields.work_item_type == kind)
.collect();
if !lines.is_empty() {
lines.push(Line::default());
}
lines.push(heading(app, &format!("{kind} ({})", children.len()), width));
for child in children {
let fields = &child.fields;
let mut spans = vec![
Span::styled(
format!(" #{} ", child.id),
Style::new().fg(type_color(app, kind)),
),
Span::raw(fields.title.clone()),
Span::raw(" "),
badge(&fields.state, state_color(app, &fields.state)),
];
if let Some(assignee) = &fields.assigned_to {
spans.push(Span::styled(format!(" · {}", assignee.display_name), muted));
}
lines.push(Line::from(spans));
}
}
lines
}
fn comments(app: &App, details: &WorkItemDetails, width: u16) -> Vec<Line<'static>> {
let theme = &app.theme;
let muted = Style::new().fg(theme.muted);
if details.comments.is_empty() {
return empty(app, "No comments");
}
let bar = Span::styled("│ ", Style::new().fg(theme.border));
let mut lines = Vec::new();
for comment in &details.comments {
lines.push(Line::from(vec![
Span::styled(
comment.author.clone(),
Style::new()
.fg(theme.comment_author)
.add_modifier(Modifier::BOLD),
),
Span::styled(format!(" · {}", short_date(&comment.date)), muted),
]));
lines.extend(wrap(&comment.text, width, bar.clone()));
lines.push(Line::default());
}
let shown = details.comments.len();
if (details.comment_count as usize) > shown {
lines.push(Line::styled(
format!(
"Showing the {shown} newest of {} comments",
details.comment_count
),
muted,
));
}
lines
}