use crate::widgets::WidgetSection;
#[derive(Debug, Clone)]
pub struct AdminView {
path: String,
title: String,
subtitle: Option<String>,
icon: Option<String>,
group: Option<String>,
permission: Option<String>,
hidden: bool,
sections: Vec<WidgetSection>,
}
fn normalize_path(raw: &str) -> String {
raw.split('/')
.filter(|seg| !seg.is_empty())
.collect::<Vec<_>>()
.join("/")
}
impl AdminView {
pub fn new(path: impl Into<String>, title: impl Into<String>) -> Self {
Self {
path: normalize_path(&path.into()),
title: title.into(),
subtitle: None,
icon: None,
group: None,
permission: None,
hidden: false,
sections: Vec::new(),
}
}
pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
self.subtitle = Some(subtitle.into());
self
}
pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn with_group(mut self, group: impl Into<String>) -> Self {
self.group = Some(group.into());
self
}
pub fn with_permission(mut self, codename: impl Into<String>) -> Self {
self.permission = Some(codename.into());
self
}
pub fn hide(mut self) -> Self {
self.hidden = true;
self
}
pub fn section(mut self, section: WidgetSection) -> Self {
self.sections.push(section);
self
}
pub fn add_sections(mut self, sections: impl IntoIterator<Item = WidgetSection>) -> Self {
self.sections.extend(sections);
self
}
pub(crate) fn path(&self) -> &str {
&self.path
}
pub(crate) fn slug(&self) -> &str {
&self.path
}
pub(crate) fn title(&self) -> &str {
&self.title
}
pub(crate) fn subtitle(&self) -> Option<&str> {
self.subtitle.as_deref()
}
pub(crate) fn icon(&self) -> Option<&str> {
self.icon.as_deref()
}
pub(crate) fn group(&self) -> Option<&str> {
self.group.as_deref()
}
pub(crate) fn permission(&self) -> Option<&str> {
self.permission.as_deref()
}
pub(crate) fn hidden(&self) -> bool {
self.hidden
}
pub(crate) fn sections(&self) -> &[WidgetSection] {
&self.sections
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widgets::WidgetSection;
#[test]
fn normalizes_path_and_slug() {
let v = AdminView::new("/reports/sales/", "Sales");
assert_eq!(
v.path(),
"reports/sales",
"leading/trailing slashes stripped"
);
assert_eq!(
v.slug(),
"reports/sales",
"slug mirrors the normalized path"
);
assert_eq!(v.title(), "Sales");
let v2 = AdminView::new("reports//sales", "X");
assert_eq!(v2.path(), "reports/sales", "double slashes collapsed");
}
#[test]
fn defaults_are_sane() {
let v = AdminView::new("tools/x", "X");
assert!(v.subtitle().is_none());
assert!(v.icon().is_none());
assert!(
v.group().is_none(),
"group defaults to None (renders under 'Pages')"
);
assert!(v.permission().is_none(), "no permission = any staff");
assert!(!v.hidden(), "shown in sidebar by default");
assert!(v.sections().is_empty());
}
#[test]
fn builders_populate_fields() {
let v = AdminView::new("reports/sales", "Sales")
.with_subtitle("Revenue")
.with_icon("bar-chart")
.with_group("Reports")
.with_permission("reports.view_sales")
.hide()
.section(WidgetSection::new("This month"));
assert_eq!(v.subtitle(), Some("Revenue"));
assert_eq!(v.icon(), Some("bar-chart"));
assert_eq!(v.group(), Some("Reports"));
assert_eq!(v.permission(), Some("reports.view_sales"));
assert!(v.hidden());
assert_eq!(v.sections().len(), 1);
}
#[test]
fn add_sections_appends() {
let v2 = AdminView::new("x", "X")
.add_sections(vec![WidgetSection::new("A"), WidgetSection::new("B")]);
assert_eq!(v2.sections().len(), 2, "add_sections appends all");
}
}