use rustlavel::prelude::*;
use crate::models::menu_item::{self, MenuItem};
use crate::models::user::User;
pub async fn shell(req: &Request, nav: &str) -> ViewContext {
let (name, description, locale) = match req.state::<crate::support::settings::Settings>() {
Some(settings) => (
settings.get("app.name").await,
settings.get("app.description").await,
settings.get("app.locale").await,
),
None => (req.config().string("app.name", "Rustlavel"), String::new(), "en".into()),
};
let mut context = ViewContext::new()
.with("app_name", Json::from(name.as_str()))
.with("app_description", Json::from(description.as_str()))
.with("app_locale", Json::from(if locale.is_empty() { "en" } else { locale.as_str() }))
.with("app_initial", Json::from(name.chars().next().unwrap_or('R').to_string()))
.with("nav", Json::from(nav))
.with("csrf_field", Json::from(rustlavel::auth::csrf::field(req)))
.with(
"csrf_token",
Json::from(rustlavel::auth::csrf::token(req).unwrap_or_default()),
);
let theme = req.cookie("theme").filter(|t| t == "dark" || t == "light").unwrap_or_else(|| "light".into());
context = context
.with("theme", Json::from(theme.as_str()))
.with("theme_class", Json::from(if theme == "dark" { "dark" } else { "" }))
.with("theme_next", Json::from(if theme == "dark" { "light" } else { "dark" }));
let errors = req.errors();
let old = req.old();
let mut summary: Option<String> = None;
if let Some(fields) = errors.as_object() {
for (field, messages) in fields {
if let Some(message) = messages.as_array().and_then(|m| m.first()).and_then(Json::as_str) {
if summary.is_none() {
summary = Some(message.to_string());
}
context = context.with(format!("error_{field}"), Json::from(message));
}
}
}
if let Some(fields) = old.as_object() {
for (field, value) in fields {
context = context.with(format!("old_{field}"), value.clone());
}
}
context = context
.with("errors", errors)
.with("old", old)
.with("error_summary", summary.map_or(Json::Null, Json::from));
let session = req.session();
let message = session.forget("flash_message").and_then(|v| v.as_str().map(str::to_string));
let kind = session.forget("flash_kind").and_then(|v| v.as_str().map(str::to_string));
context = context
.with("flash_message", message.map_or(Json::Null, Json::from))
.with("flash_kind", Json::from(kind.unwrap_or_else(|| "success".into())));
context
}
pub async fn with_user(mut context: ViewContext, req: &Request, user: &User) -> Result<ViewContext> {
context = context
.with("user_name", Json::from(user.name.as_str()))
.with("user_email", Json::from(user.email.as_str()))
.with("user_initials", Json::from(user.initials()))
.with("first_name", Json::from(user.first_name()));
let impersonating = rustlavel::auth::Impersonation::is_impersonating(req.session());
context = context.with("impersonating", Json::from(impersonating));
let menu = sidebar(req).await?;
context = context
.with("menu_custom", Json::from(!menu.is_empty()))
.with("menu", Json::Array(menu));
context = context.with("dashboard_url", Json::from(crate::support::home::path(req).await));
for (flag, permission) in [
("can_view_users", "users.view"),
("can_view_roles", "roles.view"),
("can_view_permissions", "permissions.view"),
("can_manage_settings", "settings.manage"),
("can_view_menus", "menus.view"),
("can_view_audit", "audit.view"),
{{module_nav_flags}} ] {
context = context.with(flag, Json::from(req.can(permission).await?));
}
Ok(context)
}
pub async fn sidebar(req: &Request) -> Result<Vec<Json>> {
let Some(db) = req.state::<Database>() else { return Ok(Vec::new()) };
let items = MenuItem::get(db, MenuItem::in_location("sidebar")).await?;
if items.is_empty() {
return Ok(Vec::new());
}
let granted = req.permission_list().await.unwrap_or_default();
Ok(entries(items, &granted))
}
pub fn entries(items: Vec<MenuItem>, granted: &[String]) -> Vec<Json> {
let live: Vec<MenuItem> = items.into_iter().filter(|item| item.is_active).collect();
let mut drawn = Vec::new();
for node in menu_item::flatten(&menu_item::tree(live)) {
if let Some(permission) = node.item.permission.as_deref().filter(|p| !p.is_empty())
&& !granted.iter().any(|held| held == permission)
{
continue;
}
let href = node.item.href();
drawn.push(Json::object([
("label", Json::from(node.item.label.as_str())),
("href", Json::from(href.as_str())),
(
"icon",
Json::from(crate::controllers::admin::menu_controller::icon(
node.item.icon.as_deref(),
)),
),
("depth", Json::from(node.depth as i64)),
("external", Json::from(node.item.is_external())),
("heading", Json::from(href.starts_with('#'))),
]));
}
drawn
}
pub fn flash(req: &Request, kind: &str, message: impl Into<String>) {
let session = req.session();
session.put("flash_message", Json::from(message.into()));
session.put("flash_kind", Json::from(kind));
}
pub fn old(context: ViewContext, fields: &[(&str, Option<String>)]) -> ViewContext {
let mut context = context;
for (name, value) in fields {
context = context.with(
format!("old_{name}"),
Json::from(value.clone().unwrap_or_default()),
);
}
context
}
pub fn check(values: &[(&str, &str)], rules: &[(&str, &str)]) -> rustlavel::validation::Errors {
let input = Json::object(values.iter().map(|(name, value)| (*name, Json::from(*value))));
rustlavel::validation::Validator::new(&input).rules(rules).errors()
}
pub fn errors(context: ViewContext, errors: &rustlavel::validation::Errors) -> ViewContext {
let mut context = context;
let mut first: Option<String> = None;
for (field, messages) in errors.all() {
if let Some(message) = messages.first() {
if first.is_none() {
first = Some(message.clone());
}
context = context.with(format!("error_{field}"), Json::from(message.as_str()));
}
}
context.with("error_summary", first.map_or(Json::Null, Json::from))
}
#[cfg(test)]
mod sidebar_tests {
use super::*;
fn item(id: i64, label: &str, route: &str, permission: Option<&str>, parent: Option<i64>) -> MenuItem {
MenuItem {
id,
location: "sidebar".into(),
parent_id: parent,
label: label.into(),
route: Some(route.into()),
url: None,
icon: None,
permission: permission.map(str::to_string),
sort_order: id,
is_active: true,
target: None,
}
}
fn labels(entries: &[Json]) -> Vec<String> {
entries
.iter()
.filter_map(|e| e.get("label").and_then(|l| l.as_str()).map(str::to_string))
.collect()
}
#[test]
fn an_item_with_no_permission_is_drawn_for_anybody_signed_in() {
let entries = entries(vec![item(1, "Reports", "/reports", None, None)], &[]);
assert_eq!(labels(&entries), ["Reports"]);
}
#[test]
fn an_item_is_drawn_only_for_somebody_who_holds_its_permission() {
let rows = vec![item(1, "Openings", "/ats/job/opening", Some("ats.job.opening"), None)];
assert!(labels(&entries(rows.clone(), &[])).is_empty());
assert!(labels(&entries(rows.clone(), &["users.view".into()])).is_empty());
assert_eq!(labels(&entries(rows, &["ats.job.opening".into()])), ["Openings"]);
}
#[test]
fn a_child_follows_its_parent_and_carries_its_depth() {
let rows = vec![
item(1, "ATS", "#ats", None, None),
item(2, "Openings", "/ats/job/opening", None, Some(1)),
];
let entries = entries(rows, &[]);
assert_eq!(labels(&entries), ["ATS", "Openings"]);
assert_eq!(entries[0].get("depth").and_then(|d| d.as_f64()), Some(0.0));
assert_eq!(entries[1].get("depth").and_then(|d| d.as_f64()), Some(1.0));
}
#[test]
fn an_item_pointing_at_a_fragment_is_a_heading_not_a_link() {
let group = entries(vec![item(1, "ATS", "#ats", None, None)], &[]);
assert_eq!(group[0].get("heading").and_then(|h| h.as_bool()), Some(true));
let link = entries(vec![item(1, "Reports", "/reports", None, None)], &[]);
assert_eq!(link[0].get("heading").and_then(|h| h.as_bool()), Some(false));
}
#[test]
fn an_item_switched_off_is_not_drawn() {
let mut off = item(1, "Hidden", "/hidden", None, None);
off.is_active = false;
assert!(labels(&entries(vec![off], &[])).is_empty());
}
}