navigation_dto.rs
371 lignes · 10719 octets
use rustwarden_core::i18n::extractor::Locale; use rustwarden_core::i18n::service::I18nService; /// Navigation Data Transfer Objects /// Feature 004: UI Post-Auth /// /// Defines the navigation menu structure for the application UI. use serde::{Deserialize, Serialize}; use std::sync::Arc; /// A single navigation item #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NavigationItemDTO { /// Item identifier (e.g., "home", "settings") pub id: String, /// Display label pub label: String, /// Icon name (e.g., "home", "settings") pub icon: String, /// Target URL pub url: String, /// Whether this item is active (current page) pub active: bool, /// Whether this item is enabled pub enabled: bool, /// Badge text (e.g., "New") - empty string if None (for Askama compatibility) pub badge: String, /// Helper flag for Askama compatibility (true if badge is not empty) #[serde(skip_serializing, skip_deserializing)] pub has_badge: bool, /// Message when disabled - empty string if None (for Askama compatibility) pub disabled_message: String, /// Helper flag for Askama compatibility (true if disabled_message is not empty) #[serde(skip_serializing, skip_deserializing)] pub has_disabled_message: bool, /// Order for sorting (lower = first, used internally) #[serde(skip_serializing, skip_deserializing)] pub order: i32, } impl NavigationItemDTO { /// Create an active navigation item /// /// # Examples /// /// ``` /// use rustwarden_ui::dto::navigation_dto::NavigationItemDTO; /// /// let item = NavigationItemDTO::active( /// "home", /// "Home", /// "home", /// "/home" /// ); /// /// assert!(item.active); /// assert!(item.enabled); /// ``` pub fn active(id: &str, label: &str, icon: &str, url: &str) -> Self { Self { id: id.to_string(), label: label.to_string(), icon: icon.to_string(), url: url.to_string(), active: true, enabled: true, badge: String::new(), has_badge: false, disabled_message: String::new(), has_disabled_message: false, order: 0, } } /// Create a disabled navigation item /// /// # Examples /// /// ``` /// use rustwarden_ui::dto::navigation_dto::NavigationItemDTO; /// /// let item = NavigationItemDTO::disabled( /// "module", /// "Module", /// "box", /// "/module", /// "Soon", /// Some("Feature in development") /// ); /// /// assert!(!item.active); /// assert!(!item.enabled); /// assert_eq!(item.badge, "Soon".to_string()); /// ``` pub fn disabled( id: &str, label: &str, icon: &str, url: &str, badge: &str, message: Option<&str>, ) -> Self { let badge_str = badge.to_string(); let disabled_msg = message.map(|s| s.to_string()).unwrap_or_default(); Self { id: id.to_string(), label: label.to_string(), icon: icon.to_string(), url: url.to_string(), active: false, enabled: false, badge: badge_str.clone(), has_badge: !badge_str.is_empty(), disabled_message: disabled_msg.clone(), has_disabled_message: !disabled_msg.is_empty(), order: 0, } } /// Create a regular (enabled but not active) navigation item /// /// # Examples /// /// ``` /// use rustwarden_ui::dto::navigation_dto::NavigationItemDTO; /// /// let item = NavigationItemDTO::regular( /// "settings", /// "Settings", /// "settings", /// "/settings" /// ); /// /// assert!(!item.active); /// assert!(item.enabled); /// ``` pub fn regular(id: &str, label: &str, icon: &str, url: &str) -> Self { Self { id: id.to_string(), label: label.to_string(), icon: icon.to_string(), url: url.to_string(), active: false, enabled: true, badge: String::new(), has_badge: false, disabled_message: String::new(), has_disabled_message: false, order: 0, } } } /// Complete navigation menu #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NavigationDTO { /// Primary navigation items pub items: Vec<NavigationItemDTO>, /// Secondary navigation items (e.g., user menu) pub secondary_items: Vec<NavigationItemDTO>, /// Feature 026: Flag indicating if user has a pending password change request pub has_pending_password_change: bool, } impl NavigationDTO { /// Create a new navigation with given items pub fn new(items: Vec<NavigationItemDTO>, secondary_items: Vec<NavigationItemDTO>) -> Self { Self { items, secondary_items, has_pending_password_change: false, } } } /// Default navigation for RustWarden /// /// Provides the standard navigation menu with: /// - Home (enabled) /// - Settings (enabled) impl Default for NavigationDTO { fn default() -> Self { Self { items: vec![NavigationItemDTO::regular( "home", "Accueil", "home", "/home", )], secondary_items: vec![], has_pending_password_change: false, } } } impl NavigationDTO { /// Generate navigation menu based on user roles /// /// Creates a contextualized navigation menu that includes: /// - Home (enabled) /// - Settings (enabled) /// - Administration (enabled, only if user has "admin" role) /// /// # Arguments /// /// * `roles` - Slice of role strings for the current user /// * `i18n_service` - I18nService for translations /// * `locale` - Locale for translations /// /// # Examples /// /// ```no_run /// use rustwarden_ui::dto::navigation_dto::NavigationDTO; /// use rustwarden_core::i18n::service::I18nService; /// use rustwarden_core::i18n::extractor::Locale; /// use std::sync::Arc; /// /// # async fn example() { /// let i18n_service = Arc::new(I18nService::new()); /// let locale = Locale("fr".to_string()); /// /// // Non-admin user /// let nav = NavigationDTO::for_user(&[], &i18n_service, &locale, None).await; /// assert_eq!(nav.items.len(), 1); /// /// // Admin user /// let nav = NavigationDTO::for_user(&["admin".to_string()], &i18n_service, &locale, None).await; /// assert_eq!(nav.items.len(), 2); /// assert_eq!(nav.items[1].id, "admin"); /// # } /// ``` pub async fn for_user( roles: &[String], i18n_service: &Arc<I18nService>, locale: &Locale, _db: Option<&sea_orm::DatabaseConnection>, ) -> Self { let locale_str = locale.as_str(); let home_label = i18n_service.translate("nav.home", locale_str).await; let admin_label = i18n_service.translate("nav.admin", locale_str).await; let mut items = vec![NavigationItemDTO { id: "home".to_string(), label: home_label, icon: "home".to_string(), url: "/home".to_string(), active: false, enabled: true, badge: String::new(), has_badge: false, disabled_message: String::new(), has_disabled_message: false, order: 0, }]; if roles.contains(&"admin".to_string()) { items.push(NavigationItemDTO { id: "admin".to_string(), label: admin_label, icon: "shield".to_string(), url: "/admin".to_string(), active: false, enabled: true, badge: String::new(), has_badge: false, disabled_message: String::new(), has_disabled_message: false, order: 100, }); } items.sort_by_key(|item| item.order); let has_pending_password_change = false; Self { items, secondary_items: vec![], has_pending_password_change, } } /// Set the pending password change flag (Feature 026) pub fn set_pending_password_change(&mut self, has_pending: bool) { self.has_pending_password_change = has_pending; } } #[cfg(test)] mod tests { use super::*; #[test] fn test_navigation_item_active() { let item = NavigationItemDTO::active("home", "Home", "home", "/home"); assert_eq!(item.id, "home"); assert_eq!(item.label, "Home"); assert_eq!(item.icon, "home"); assert_eq!(item.url, "/home"); assert!(item.active); assert!(item.enabled); assert!(item.badge.is_empty()); assert!(item.disabled_message.is_empty()); } #[test] fn test_navigation_item_disabled() { let item = NavigationItemDTO::disabled( "module", "Module", "box", "/module", "Soon", Some("Coming soon"), ); assert_eq!(item.id, "module"); assert_eq!(item.label, "Module"); assert_eq!(item.icon, "box"); assert_eq!(item.url, "/module"); assert!(!item.active); assert!(!item.enabled); assert_eq!(item.badge, "Soon"); assert_eq!(item.disabled_message, "Coming soon"); } #[test] fn test_navigation_item_regular() { let item = NavigationItemDTO::regular("settings", "Settings", "settings", "/settings"); assert_eq!(item.id, "settings"); assert!(!item.active); assert!(item.enabled); assert!(item.badge.is_empty()); } #[test] fn test_navigation_dto_default() { let nav = NavigationDTO::default(); assert_eq!(nav.items.len(), 1); assert_eq!(nav.secondary_items.len(), 0); let home = &nav.items[0]; assert_eq!(home.id, "home"); assert!(home.enabled); } #[test] fn test_navigation_dto_new() { let items = vec![NavigationItemDTO::active("home", "Home", "home", "/home")]; let secondary = vec![NavigationItemDTO::regular( "logout", "Logout", "logout", "/logout", )]; let nav = NavigationDTO::new(items, secondary); assert_eq!(nav.items.len(), 1); assert_eq!(nav.secondary_items.len(), 1); } }
GitRust