#[allow(clippy::wildcard_imports)]
use super::*;
impl ProjectPartition {
pub(super) fn build(config: &crate::config::Config, issues: &[Issue]) -> Self {
let index = crate::projects::ProjectIndex::build(config, issues);
let order: HashMap<String, usize> = index
.projects()
.iter()
.enumerate()
.map(|(position, project)| (project.id.to_string(), position))
.collect();
Self {
memberships: index.resolved_memberships(),
order,
}
}
pub(super) fn memberships(&self, number: u32) -> Vec<String> {
self.memberships.get(&number).cloned().unwrap_or_default()
}
pub(super) fn rank(&self, number: u32) -> usize {
match self.memberships.get(&number).map(Vec::as_slice) {
None | Some([]) => usize::MAX,
Some([only]) => self.order.get(only).copied().unwrap_or(usize::MAX),
Some(_) => usize::MAX - 1,
}
}
}
pub(crate) fn project_bucket_label(projects: &[String]) -> String {
crate::projects::project_bucket_label(projects)
}
pub(crate) struct EpicPartition {
pub(super) memberships: HashMap<u32, Vec<String>>,
pub(super) order: HashMap<String, usize>,
}
impl EpicPartition {
pub(super) fn build(config: &crate::config::Config, issues: &[Issue]) -> Self {
let index = crate::epics::EpicIndex::build(config, issues);
let order: HashMap<String, usize> = index
.epics()
.iter()
.enumerate()
.map(|(position, epic)| (epic.id.to_string(), position))
.collect();
let memberships = index.memberships_by_issue();
Self { memberships, order }
}
pub(super) fn memberships(&self, number: u32) -> Vec<String> {
self.memberships.get(&number).cloned().unwrap_or_default()
}
pub(super) fn rank(&self, number: u32) -> usize {
match self.memberships.get(&number).map(Vec::as_slice) {
None | Some([]) => usize::MAX,
Some([only]) => self.order.get(only).copied().unwrap_or(usize::MAX),
Some(_) => usize::MAX - 1,
}
}
}
pub(crate) fn epic_bucket_label(ids: &[String]) -> String {
crate::search::epic_bucket_label(ids)
}
impl crate::config::OrderBy {
pub fn sort(&self, cards: &mut [Card]) {
match self {
crate::config::OrderBy::CreatedAtDesc => {
cards.sort_by(|a, b| b.created.cmp(&a.created))
}
crate::config::OrderBy::CreatedAtAsc => cards.sort_by(|a, b| a.created.cmp(&b.created)),
crate::config::OrderBy::None => {}
}
}
}
pub(crate) fn sort_by_default_order(board_config: &BoardConfiguration, cards: &mut [Card]) {
match board_config.order_by {
crate::config::OrderBy::CreatedAtDesc => {
cards.sort_by(|a, b| {
b.created
.cmp(&a.created)
.then_with(|| a.filename.cmp(&b.filename))
});
}
crate::config::OrderBy::CreatedAtAsc => {
cards.sort_by(|a, b| {
a.created
.cmp(&b.created)
.then_with(|| a.filename.cmp(&b.filename))
});
}
crate::config::OrderBy::None => {
cards.sort_by(|a, b| a.filename.cmp(&b.filename));
}
}
}
pub(crate) fn sort_cards(board_config: &BoardConfiguration, cards: &mut [Card]) {
if board_config.card_order.is_empty() {
sort_by_default_order(board_config, cards);
return;
}
let mut baseline = cards.to_vec();
sort_by_default_order(board_config, &mut baseline);
let manual_cards: Vec<Card> = board_config
.card_order
.iter()
.filter_map(|filename| {
baseline
.iter()
.find(|card| card.filename == *filename)
.cloned()
})
.collect();
let touched: HashSet<String> = manual_cards
.iter()
.map(|card| card.filename.clone())
.collect();
let mut manual_iter = manual_cards.into_iter();
let mut ordered = Vec::with_capacity(baseline.len());
for card in baseline {
if touched.contains(card.filename.as_str()) {
if let Some(manual_card) = manual_iter.next() {
ordered.push(manual_card);
} else {
ordered.push(card);
}
} else {
ordered.push(card);
}
}
cards.clone_from_slice(&ordered);
}
pub(crate) fn nest_children(cards: &mut Vec<Card>) {
let count = cards.len();
if count == 0 {
return;
}
let position: std::collections::HashMap<crate::id::IssueRef, usize> = cards
.iter()
.enumerate()
.map(|(i, card)| (card.reference, i))
.collect();
let mut adopted: Vec<Vec<usize>> = vec![Vec::new(); count];
let mut top_level: Vec<usize> = Vec::with_capacity(count);
for (index, card) in cards.iter().enumerate() {
match card
.parent
.and_then(|parent| position.get(&parent).copied())
{
Some(parent)
if cards[parent]
.parent
.is_none_or(|grandparent| !position.contains_key(&grandparent)) =>
{
adopted[parent].push(index);
}
_ => top_level.push(index),
}
}
let mut ordered: Vec<Card> = Vec::with_capacity(count);
for index in top_level {
let mut card = cards[index].clone();
card.depth = 0;
ordered.push(card);
for &child in &adopted[index] {
let mut child_card = cards[child].clone();
child_card.depth = 1;
ordered.push(child_card);
}
}
*cards = ordered;
}
impl BoardState {
pub fn build_filtered(
board_config: &BoardConfiguration,
config: &crate::config::Config,
corpus: &Corpus,
query: &str,
filter: &StructuredFilter,
resolution: &crate::workflow::CategoryResolution,
) -> Self {
Self::build_filtered_with(
board_config,
config,
corpus,
query,
filter,
resolution,
false,
)
}
pub fn build_filtered_with(
board_config: &BoardConfiguration,
config: &crate::config::Config,
corpus: &Corpus,
query: &str,
filter: &StructuredFilter,
resolution: &crate::workflow::CategoryResolution,
show_children: bool,
) -> Self {
let show_mismatch = board_config.show_mismatch;
let done_column = crate::workflow::completion_column_or_default(board_config, config);
let done_column = done_column.as_str();
let blocked = blocked_reasons(&corpus.issues, done_column);
let children = child_markers(&corpus.issues, done_column);
let filtering = !query.is_empty() || filter.has_active_criteria();
let sort_key = crate::search::control_token(query, "sort")
.or_else(|| crate::search::control_token(query, "group"));
let projects = ProjectPartition::build(config, &corpus.issues);
let epics = EpicPartition::build(config, &corpus.issues);
let number = crate::search::number_query(query);
let hidden_names: Vec<&str> = board_config
.hidden_columns
.iter()
.map(String::as_str)
.collect();
let mut column_map: Vec<Vec<Card>> =
board_config.columns.iter().map(|_| Vec::new()).collect();
let mut mismatch_cards = Vec::new();
let mut matched_issues: Vec<&crate::model::Issue> = Vec::new();
for (issue, text) in corpus.issues.iter().zip(corpus.texts().iter()) {
let keep = if let Some(n) = number {
issue.number == n && filter.evaluate_issue(issue, text, resolution)
} else {
crate::search::matches_issue(query, filter, issue, text, resolution)
};
if !keep {
continue;
}
matched_issues.push(issue);
}
Self::sort_filtered_issues(&mut matched_issues, sort_key.as_deref(), &projects, &epics);
let health_badges = health_badges(&corpus.issues);
for issue in matched_issues {
let card = Card {
reference: crate::id::canonical_ref(issue),
number: issue.number,
title: issue.title.clone(),
priority: issue.priority.clone(),
labels: issue.labels.clone(),
assignee: issue.assignee.clone(),
filename: issue.filename.clone(),
commit: issue.commit.clone(),
created: issue.created.clone(),
due_date: issue.due_date.clone(),
status: issue.status.display().to_string(),
category: crate::workflow::category_of(issue, resolution)
.map(|c| c.label().to_string()),
category_color: crate::workflow::category_of(issue, resolution)
.and_then(|c| c.color_hint()),
done: crate::workflow::is_completed(issue, resolution),
cycle: issue.cycles.as_ref().and_then(|v| v.first().cloned()),
milestone: issue.milestones.as_ref().and_then(|v| v.first().cloned()),
blocked: blocked.get(&issue.number).cloned(),
children: children.get(&issue.number).cloned(),
parent: issue.parent,
depth: 0,
archived: issue.archived,
related: issue.related_issues.len(),
health: health_badges.get(&issue.number).cloned(),
projects: projects.memberships(issue.number),
epics: epics.memberships(issue.number),
};
if let Some(idx) = board_config
.columns
.iter()
.position(|c| c == issue.status.display())
{
column_map[idx].push(card);
} else {
mismatch_cards.push(card);
}
}
if sort_key.is_none() {
for cards in column_map.iter_mut() {
sort_cards(board_config, cards);
}
sort_cards(board_config, &mut mismatch_cards);
}
let mut columns: Vec<BoardColumn> = Vec::new();
let mut hidden: Vec<BoardColumn> = Vec::new();
for (cards, name) in column_map.into_iter().zip(board_config.columns.iter()) {
let col = BoardColumn {
name: name.clone(),
cards,
};
if hidden_names.contains(&name.as_str()) {
if filtering {
columns.push(col);
} else {
hidden.push(col);
}
} else {
columns.push(col);
}
}
let mut state = BoardState {
columns,
hidden,
mismatch: BoardColumn {
name: "Mismatched".to_string(),
cards: mismatch_cards,
},
show_mismatch,
active_col: 0,
selected: 0,
filter: Some(query.to_string()),
hover: None,
hover_col: None,
};
state.apply_child_order(show_children);
state
}
pub(super) fn sort_filtered_issues(
issues: &mut [&crate::model::Issue],
sort: Option<&str>,
projects: &ProjectPartition,
epics: &EpicPartition,
) {
match sort.unwrap_or("created") {
"project" => issues.sort_by(|a, b| {
projects
.rank(a.number)
.cmp(&projects.rank(b.number))
.then(a.number.cmp(&b.number))
}),
"epic" => issues.sort_by(|a, b| {
epics
.rank(a.number)
.cmp(&epics.rank(b.number))
.then(a.number.cmp(&b.number))
}),
"due" => {
issues.sort_by(|a, b| a.due_date.cmp(&b.due_date).then(a.number.cmp(&b.number)))
}
"start" => issues.sort_by(|a, b| {
a.start_date
.cmp(&b.start_date)
.then(a.number.cmp(&b.number))
}),
"reviewer" => {
issues.sort_by(|a, b| a.reviewer.cmp(&b.reviewer).then(a.number.cmp(&b.number)))
}
"cycle" => issues.sort_by(|a, b| {
a.cycles
.as_ref()
.and_then(|v| v.first())
.cmp(&b.cycles.as_ref().and_then(|v| v.first()))
.then(a.number.cmp(&b.number))
}),
"milestone" => issues.sort_by(|a, b| {
a.milestones
.as_ref()
.and_then(|v| v.first())
.cmp(&b.milestones.as_ref().and_then(|v| v.first()))
.then(a.number.cmp(&b.number))
}),
"status" => issues.sort_by(|a, b| {
a.status
.display()
.cmp(b.status.display())
.then(a.number.cmp(&b.number))
}),
"priority" => issues.sort_by(|a, b| {
a.priority
.to_string()
.cmp(&b.priority.to_string())
.then(a.number.cmp(&b.number))
}),
_ => issues.sort_by(|a, b| a.created.cmp(&b.created).then(a.number.cmp(&b.number))),
}
}
}