use tablo_ui::{
breadcrumb, breadcrumb_item, breadcrumb_link, breadcrumb_list, breadcrumb_page,
breadcrumb_separator,
};
use topcoat::{
context::Cx,
view::{BoxView, ViewExt, view},
};
use crate::{
Text,
navigation::{NavigationItem, current_index},
};
#[derive(Debug, PartialEq, Eq)]
pub(super) struct Crumb {
pub(super) label: Text,
pub(super) href: Option<String>,
}
impl Crumb {
fn link(label: impl Into<Text>, href: impl Into<String>) -> Self {
Self {
label: label.into(),
href: Some(href.into()),
}
}
fn here(label: impl Into<Text>) -> Self {
Self {
label: label.into(),
href: None,
}
}
}
pub(super) fn trail(
nav_items: &[NavigationItem],
resource_lists: &[&str],
current_path: &str,
current_query: &str,
) -> Vec<Crumb> {
let Some((item, path)) = entry(nav_items, current_path, current_query) else {
return Vec::new();
};
let rest: Vec<&str> = current_path[path.len()..]
.split('/')
.filter(|segment| !segment.is_empty())
.collect();
let entry = || Crumb::link(item.label.clone(), path);
if !resource_lists.contains(&path) {
return match rest.last() {
None => vec![Crumb::here(item.label.clone())],
Some(last) => vec![entry(), Crumb::here(humanize(last))],
};
}
match rest.as_slice() {
[] => vec![Crumb::here(item.label.clone())],
["-", "create"] => vec![entry(), Crumb::here(Text::message("tablo-crumb-create"))],
["-", action] => vec![entry(), Crumb::here(humanize(action))],
[_record] => vec![entry(), Crumb::here(Text::message("tablo-crumb-view"))],
[_record, "edit"] => vec![entry(), Crumb::here(Text::message("tablo-crumb-edit"))],
[record, .., last] => vec![
entry(),
Crumb::link(
Text::message("tablo-crumb-view"),
format!("{path}/{record}"),
),
Crumb::here(humanize(last)),
],
}
}
pub(super) fn failed(
nav_items: &[NavigationItem],
current_path: &str,
current_query: &str,
label: impl Into<Text>,
) -> Vec<Crumb> {
let mut crumbs: Vec<Crumb> = entry(nav_items, current_path, current_query)
.map(|(item, path)| Crumb::link(item.label.clone(), path))
.into_iter()
.collect();
crumbs.push(Crumb::here(label));
crumbs
}
fn entry<'a>(
nav_items: &'a [NavigationItem],
current_path: &str,
current_query: &str,
) -> Option<(&'a NavigationItem, &'a str)> {
let item = &nav_items[current_index(nav_items, current_path, current_query)?];
Some((item, item.path()?))
}
fn humanize(segment: &str) -> String {
let decoded = percent_encoding::percent_decode_str(segment).decode_utf8_lossy();
let words = decoded.replace(['-', '_'], " ");
let mut chars = words.chars();
chars
.next()
.map(|first| first.to_uppercase().chain(chars).collect())
.unwrap_or_default()
}
pub(super) fn render(cx: &Cx, crumbs: Vec<Crumb>) -> BoxView<'_> {
if crumbs.is_empty() {
return ().boxed();
}
let last = crumbs.len() - 1;
view! {
cx =>
breadcrumb(
attrs: topcoat::view::attributes! { class="max-sm:hidden" },
breadcrumb_list(
for (index, crumb) in crumbs.into_iter().enumerate() {
breadcrumb_item(
if let Some(href) = crumb.href {
breadcrumb_link(
attrs: crate::navigation::runtime_link(cx, &href),
(crumb.label.localize(cx))
)
} else {
breadcrumb_page((crumb.label.localize(cx)))
}
)
if index < last {
breadcrumb_separator()
}
}
)
)
}
.boxed()
}
#[cfg(test)]
mod tests;