use pinto::backlog::{BacklogItem, ItemId};
use pinto::service::{Board, Forest, build_forest};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DisplayRow {
pub(crate) item_index: usize,
pub(crate) depth: usize,
pub(crate) child_count: usize,
pub(crate) expanded: bool,
pub(crate) parent_index: Option<usize>,
}
pub(crate) fn column_display_rows(
items: &[BacklogItem],
expanded: &HashSet<ItemId>,
) -> Vec<DisplayRow> {
let forest = build_forest(items);
let mut out = Vec::new();
let mut emitted = HashSet::new();
for &r in &forest.roots {
visit(r, 0, None, items, &forest, expanded, &mut emitted, &mut out);
}
for &i in &forest.unreachable {
if emitted.insert(i) {
out.push(DisplayRow {
item_index: i,
depth: 0,
child_count: forest.children[i].len(),
expanded: false,
parent_index: None,
});
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn visit(
i: usize,
depth: usize,
parent_index: Option<usize>,
items: &[BacklogItem],
forest: &Forest,
expanded: &HashSet<ItemId>,
emitted: &mut HashSet<usize>,
out: &mut Vec<DisplayRow>,
) {
if !emitted.insert(i) {
return;
}
let kids = &forest.children[i];
let child_count = kids.len();
let is_expanded = child_count > 0 && expanded.contains(&items[i].id);
out.push(DisplayRow {
item_index: i,
depth,
child_count,
expanded: is_expanded,
parent_index,
});
if is_expanded {
for &c in kids {
visit(c, depth + 1, Some(i), items, forest, expanded, emitted, out);
}
}
}
pub(crate) fn board_items(board: &Board) -> impl Iterator<Item = &BacklogItem> {
board
.columns
.iter()
.flat_map(|c| c.items.iter())
.chain(board.orphaned.iter())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PopupContent {
pub(crate) id: ItemId,
pub(crate) title: String,
pub(crate) status: pinto::backlog::Status,
pub(crate) rank: pinto::rank::Rank,
pub(crate) rank_ordinal: usize,
pub(crate) points: Option<u32>,
pub(crate) labels: Vec<String>,
pub(crate) assignee: Option<String>,
pub(crate) sprint: Option<String>,
pub(crate) commits: Vec<String>,
pub(crate) body: String,
pub(crate) parent: Option<String>,
pub(crate) children: Vec<ItemId>,
pub(crate) depends_on: Vec<ItemId>,
pub(crate) dependents: Vec<ItemId>,
pub(crate) start_at: Option<chrono::DateTime<chrono::Utc>>,
pub(crate) done_at: Option<chrono::DateTime<chrono::Utc>>,
pub(crate) created: chrono::DateTime<chrono::Utc>,
pub(crate) updated: chrono::DateTime<chrono::Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
use pinto::backlog::Status;
use pinto::rank::Rank;
fn item(id: &str, rank: &str, parent: Option<&str>) -> BacklogItem {
let mut it = BacklogItem::new(
id.parse::<ItemId>().unwrap(),
id.to_string(),
Status::new("todo"),
Rank::parse(rank).expect("valid rank"),
chrono::Utc::now(),
)
.unwrap();
it.parent = parent.map(|p| p.parse::<ItemId>().unwrap());
it
}
fn rows(items: &[BacklogItem], expanded: &[&str]) -> Vec<(String, usize)> {
let set: HashSet<ItemId> = expanded.iter().map(|e| e.parse().unwrap()).collect();
column_display_rows(items, &set)
.into_iter()
.map(|r| (items[r.item_index].id.to_string(), r.depth))
.collect()
}
#[test]
fn expanded_child_groups_under_its_parent_ahead_of_a_higher_ranked_root() {
let items = [
item("T-1", "a", None),
item("T-2", "m", None),
item("T-3", "z", Some("T-1")),
];
assert_eq!(
rows(&items, &["T-1"]),
[
("T-1".to_string(), 0),
("T-3".to_string(), 1),
("T-2".to_string(), 0),
],
"child nests under parent even though a later root outranks it"
);
assert_eq!(
rows(&items, &[]),
[("T-1".to_string(), 0), ("T-2".to_string(), 0)]
);
}
#[test]
fn roots_and_siblings_stay_in_canonical_rank_order() {
let items = [
item("T-1", "a", None), item("T-5", "c", None), item("T-2", "g", Some("T-1")), item("T-3", "m", Some("T-1")), item("T-4", "t", None), ];
assert_eq!(
rows(&items, &["T-1"]),
[
("T-1".to_string(), 0),
("T-2".to_string(), 1), ("T-3".to_string(), 1),
("T-5".to_string(), 0), ("T-4".to_string(), 0),
]
);
}
}