//! Commit status indicators. `commit_status` is the combined state as an icon, with a dropdown of //! every context (the commit list, commit page and PR commits tab). `checks_summary` is the line in //! the pull request merge box. use forge_core::store::commit_status::{CommitStatus, StatusState, combine}; use topcoat::Result; use topcoat::view::{View, component, view}; use crate::i18n::Lang; use crate::i18n::code::{self as g, K}; /// The language of the status indicators. It defaults to English, so a caller that does not pass /// one (the pull request page, for now) keeps its English text. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct StatusLang(pub Lang); impl Default for StatusLang { fn default() -> Self { StatusLang(Lang::En) } } /// One context's report, prepared for display. #[derive(Clone)] pub struct StatusItem { pub context: String, /// The state's icon, and the CSS classes that colour it. pub icon: &'static str, pub icon_class: String, pub description: String, pub has_description: bool, pub link: String, pub has_link: bool, } impl From<&CommitStatus> for StatusItem { fn from(status: &CommitStatus) -> Self { let description = status.description.clone().unwrap_or_default(); let link = status.target_url.clone().unwrap_or_default(); Self { context: status.context.clone(), icon: icon(status.state), icon_class: format!("status-icon status-{}", css(status.state)), has_description: !description.is_empty(), description, has_link: !link.is_empty(), link, } } } /// Everything the indicators show about one commit. #[derive(Clone, Default)] pub struct StatusSummary { /// Combined state, `None` when the commit has no statuses. pub state: Option, pub items: Vec, } impl StatusSummary { pub fn from_rows(rows: &[CommitStatus]) -> Self { Self { state: combine(rows.iter().map(|row| row.state)), items: rows.iter().map(StatusItem::from).collect(), } } } /// The icon for a state: check, cross or dot. pub fn icon(state: StatusState) -> &'static str { match state { StatusState::Success => "✓", StatusState::Failure | StatusState::Error => "✗", StatusState::Pending => "●", } } /// CSS modifier for a state. `error` shares the failure colour. pub fn css(state: StatusState) -> &'static str { match state { StatusState::Success => "success", StatusState::Failure | StatusState::Error => "failure", StatusState::Pending => "pending", } } /// The combined icon with a dropdown listing every context. Renders nothing when the commit has /// no statuses. #[component] pub async fn commit_status( summary: StatusSummary, #[default] lang: StatusLang, ) -> Result { let lang = lang.0; let has_state = summary.state.is_some(); let state = summary.state.unwrap_or(StatusState::Pending); let icon_text = icon(state); let css_class = format!("status-icon status-{}", css(state)); let title = format!( "{}{}", g::t(lang, K::CommitStatusPrefix), g::state_word(lang, state) ); let items = summary.items; Ok(view! { if has_state {
status_list(items: items, lang: lang)
} }) } /// The merge box line: "All checks have passed", "Some checks failed" or "Checks pending", with /// the contexts beneath it. #[component] pub async fn checks_summary( summary: StatusSummary, #[default] lang: StatusLang, ) -> Result { let lang = lang.0; let (text, state) = match summary.state { None => (g::t(lang, K::NoChecksReported), None), Some(StatusState::Success) => (g::t(lang, K::AllChecksPassed), Some(StatusState::Success)), Some(StatusState::Pending) => (g::t(lang, K::ChecksPending), Some(StatusState::Pending)), Some(StatusState::Failure) | Some(StatusState::Error) => { (g::t(lang, K::SomeChecksFailed), Some(StatusState::Failure)) } }; let icon_text = state.map_or("", icon); let css_class = format!("checks-summary status-{}", state.map_or("none", css)); let items = summary.items; let has_items = !items.is_empty(); Ok(view! {
(text) if has_items {
(g::t(lang, K::ShowChecks)) status_list(items: items, lang: lang)
}
}) } /// The contexts of one commit: icon, context name, description and a details link. #[component] pub async fn status_list(items: Vec, lang: Lang) -> Result { Ok(view! {
    for item in items.iter() {
  • (item.icon) (item.context.as_str()) if item.has_description { (item.description.as_str()) } if item.has_link { (g::t(lang, K::DetailsLink)) }
  • }
}) }