1use crate::widgets::WidgetSection;
17
18#[derive(Debug, Clone)]
22pub struct AdminView {
23 path: String,
24 title: String,
25 subtitle: Option<String>,
26 icon: Option<String>,
27 group: Option<String>,
28 permission: Option<String>,
29 hidden: bool,
30 sections: Vec<WidgetSection>,
31}
32
33fn normalize_path(raw: &str) -> String {
36 raw.split('/')
37 .filter(|seg| !seg.is_empty())
38 .collect::<Vec<_>>()
39 .join("/")
40}
41
42impl AdminView {
43 pub fn new(path: impl Into<String>, title: impl Into<String>) -> Self {
47 Self {
48 path: normalize_path(&path.into()),
49 title: title.into(),
50 subtitle: None,
51 icon: None,
52 group: None,
53 permission: None,
54 hidden: false,
55 sections: Vec::new(),
56 }
57 }
58
59 pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
61 self.subtitle = Some(subtitle.into());
62 self
63 }
64
65 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
67 self.icon = Some(icon.into());
68 self
69 }
70
71 pub fn with_group(mut self, group: impl Into<String>) -> Self {
73 self.group = Some(group.into());
74 self
75 }
76
77 pub fn with_permission(mut self, codename: impl Into<String>) -> Self {
79 self.permission = Some(codename.into());
80 self
81 }
82
83 pub fn hide(mut self) -> Self {
85 self.hidden = true;
86 self
87 }
88
89 pub fn section(mut self, section: WidgetSection) -> Self {
91 self.sections.push(section);
92 self
93 }
94
95 pub fn add_sections(mut self, sections: impl IntoIterator<Item = WidgetSection>) -> Self {
97 self.sections.extend(sections);
98 self
99 }
100
101 pub(crate) fn path(&self) -> &str {
104 &self.path
105 }
106
107 pub(crate) fn slug(&self) -> &str {
109 &self.path
110 }
111
112 pub(crate) fn title(&self) -> &str {
113 &self.title
114 }
115
116 pub(crate) fn subtitle(&self) -> Option<&str> {
117 self.subtitle.as_deref()
118 }
119
120 pub(crate) fn icon(&self) -> Option<&str> {
121 self.icon.as_deref()
122 }
123
124 pub(crate) fn group(&self) -> Option<&str> {
125 self.group.as_deref()
126 }
127
128 pub(crate) fn permission(&self) -> Option<&str> {
129 self.permission.as_deref()
130 }
131
132 pub(crate) fn hidden(&self) -> bool {
133 self.hidden
134 }
135
136 pub(crate) fn sections(&self) -> &[WidgetSection] {
137 &self.sections
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::widgets::WidgetSection;
145
146 #[test]
147 fn normalizes_path_and_slug() {
148 let v = AdminView::new("/reports/sales/", "Sales");
149 assert_eq!(
150 v.path(),
151 "reports/sales",
152 "leading/trailing slashes stripped"
153 );
154 assert_eq!(
155 v.slug(),
156 "reports/sales",
157 "slug mirrors the normalized path"
158 );
159 assert_eq!(v.title(), "Sales");
160
161 let v2 = AdminView::new("reports//sales", "X");
162 assert_eq!(v2.path(), "reports/sales", "double slashes collapsed");
163 }
164
165 #[test]
166 fn defaults_are_sane() {
167 let v = AdminView::new("tools/x", "X");
168 assert!(v.subtitle().is_none());
169 assert!(v.icon().is_none());
170 assert!(
171 v.group().is_none(),
172 "group defaults to None (renders under 'Pages')"
173 );
174 assert!(v.permission().is_none(), "no permission = any staff");
175 assert!(!v.hidden(), "shown in sidebar by default");
176 assert!(v.sections().is_empty());
177 }
178
179 #[test]
180 fn builders_populate_fields() {
181 let v = AdminView::new("reports/sales", "Sales")
184 .with_subtitle("Revenue")
185 .with_icon("bar-chart")
186 .with_group("Reports")
187 .with_permission("reports.view_sales")
188 .hide()
189 .section(WidgetSection::new("This month"));
190 assert_eq!(v.subtitle(), Some("Revenue"));
191 assert_eq!(v.icon(), Some("bar-chart"));
192 assert_eq!(v.group(), Some("Reports"));
193 assert_eq!(v.permission(), Some("reports.view_sales"));
194 assert!(v.hidden());
195 assert_eq!(v.sections().len(), 1);
196 }
197
198 #[test]
199 fn add_sections_appends() {
200 let v2 = AdminView::new("x", "X")
201 .add_sections(vec![WidgetSection::new("A"), WidgetSection::new("B")]);
202 assert_eq!(v2.sections().len(), 2, "add_sections appends all");
203 }
204}