diff --git a/client/src/modes/common/commands.rs b/client/src/modes/common/commands.rs index 5f7845d..a68caa4 100644 --- a/client/src/modes/common/commands.rs +++ b/client/src/modes/common/commands.rs @@ -34,7 +34,7 @@ impl CommandHandler { ) -> Result<(bool, String)> { // Use router to check unsaved changes let has_unsaved = match &router.current { - Page::Login(state) => state.has_unsaved_changes(), + Page::Login(page) => page.state.has_unsaved_changes(), Page::Register(state) => state.has_unsaved_changes(), Page::Form(fs) => fs.has_unsaved_changes, _ => false, diff --git a/client/src/modes/general/navigation.rs b/client/src/modes/general/navigation.rs index f5deb30..3cf3cb1 100644 --- a/client/src/modes/general/navigation.rs +++ b/client/src/modes/general/navigation.rs @@ -87,11 +87,11 @@ pub async fn handle_navigation_event( pub fn up(app_state: &mut AppState, router: &mut Router) { match &mut router.current { - Page::Login(state) if app_state.ui.focus_outside_canvas => { + Page::Login(page) if app_state.ui.focus_outside_canvas => { if app_state.focused_button_index == 0 { app_state.ui.focus_outside_canvas = false; - let last_field_index = state.field_count().saturating_sub(1); - state.set_current_field(last_field_index); + let last_field_index = page.state.field_count().saturating_sub(1); + page.state.set_current_field(last_field_index); } else { app_state.focused_button_index = app_state.focused_button_index.saturating_sub(1); diff --git a/client/src/pages/login/logic.rs b/client/src/pages/login/logic.rs index 9ef38e0..e0035f1 100644 --- a/client/src/pages/login/logic.rs +++ b/client/src/pages/login/logic.rs @@ -1,4 +1,4 @@ -// src/tui/functions/common/login.rs +// src/pages/login/logic.rs use crate::services::auth::AuthClient; use crate::state::pages::auth::AuthState; @@ -7,12 +7,11 @@ use crate::buffer::state::{AppView, BufferState}; use crate::config::storage::storage::{StoredAuthData, save_auth_data}; use crate::ui::handlers::context::DialogPurpose; use common::proto::komp_ac::auth::LoginResponse; -use crate::pages::login::LoginState; -use anyhow::{Context, Result}; +use crate::pages::login::LoginFormState; +use anyhow::{Context, Result, anyhow}; use tokio::spawn; use tokio::sync::mpsc; use tracing::{info, error}; -use anyhow::anyhow; #[derive(Debug)] pub enum LoginResult { @@ -25,15 +24,14 @@ pub enum LoginResult { /// Updates AuthState and AppState on success or failure. pub async fn save( auth_state: &mut AuthState, - login_state: &mut LoginState, + login_state: &mut LoginFormState, auth_client: &mut AuthClient, app_state: &mut AppState, ) -> Result { - let identifier = login_state.username.clone(); - let password = login_state.password.clone(); + let identifier = login_state.username().to_string(); + let password = login_state.password().to_string(); // --- Client-side validation --- - // Prevent login attempt if the identifier field is empty or whitespace. if identifier.trim().is_empty() { let error_message = "Username/Email cannot be empty.".to_string(); app_state.show_dialog( @@ -42,33 +40,33 @@ pub async fn save( vec!["OK".to_string()], DialogPurpose::LoginFailed, ); - login_state.error_message = Some(error_message.clone()); - return Err(anyhow::anyhow!(error_message)); + login_state.set_error_message(Some(error_message.clone())); + return Err(anyhow!(error_message)); } // Clear previous error/dialog state before attempting - login_state.error_message = None; - app_state.hide_dialog(); // Hide any previous dialog + login_state.set_error_message(None); + app_state.hide_dialog(); // Call the gRPC login method match auth_client.login(identifier.clone(), password).await .with_context(|| format!("gRPC login attempt failed for identifier: {}", identifier)) { Ok(response) => { - // Store authentication details using correct field names + // Store authentication details auth_state.auth_token = Some(response.access_token.clone()); auth_state.user_id = Some(response.user_id.clone()); auth_state.role = Some(response.role.clone()); auth_state.decoded_username = Some(response.username.clone()); - login_state.set_has_unsaved_changes(false); - login_state.error_message = None; - // Format the success message using response data + login_state.set_has_unsaved_changes(false); + login_state.set_error_message(None); + let success_message = format!( "Login Successful!\n\n\ - Username: {}\n\ - User ID: {}\n\ - Role: {}", + Username: {}\n\ + User ID: {}\n\ + Role: {}", response.username, response.user_id, response.role @@ -80,9 +78,11 @@ pub async fn save( vec!["Menu".to_string(), "Exit".to_string()], DialogPurpose::LoginSuccess, ); - login_state.password.clear(); - login_state.username.clear(); - login_state.current_cursor_pos = 0; + + login_state.username_mut().clear(); + login_state.password_mut().clear(); + login_state.set_current_cursor_pos(0); + Ok("Login successful, details shown in dialog.".to_string()) } Err(e) => { @@ -93,10 +93,10 @@ pub async fn save( vec!["OK".to_string()], DialogPurpose::LoginFailed, ); - login_state.error_message = Some(error_message.clone()); + login_state.set_error_message(Some(error_message.clone())); login_state.set_has_unsaved_changes(true); - login_state.username.clear(); - login_state.password.clear(); + login_state.username_mut().clear(); + login_state.password_mut().clear(); Err(e) } } @@ -104,56 +104,42 @@ pub async fn save( /// Reverts the login form fields to empty and returns to the previous screen (Intro). pub async fn revert( - login_state: &mut LoginState, - _app_state: &mut AppState, // Keep signature consistent if needed elsewhere + login_state: &mut LoginFormState, + app_state: &mut AppState, ) -> String { - // Clear the input fields - login_state.username.clear(); - login_state.password.clear(); - login_state.error_message = None; - login_state.set_has_unsaved_changes(false); - login_state.login_request_pending = false; // Ensure flag is reset on revert - + login_state.clear(); + app_state.hide_dialog(); "Login reverted".to_string() } +/// Clears login form and navigates back to main menu. pub async fn back_to_main( - login_state: &mut LoginState, + login_state: &mut LoginFormState, app_state: &mut AppState, buffer_state: &mut BufferState, ) -> String { - // Clear the input fields - login_state.username.clear(); - login_state.password.clear(); - login_state.error_message = None; - login_state.set_has_unsaved_changes(false); - login_state.login_request_pending = false; // Ensure flag is reset - - // Ensure dialog is hidden if revert is called + login_state.clear(); app_state.hide_dialog(); - // Navigation logic buffer_state.close_active_buffer(); buffer_state.update_history(AppView::Intro); - // Reset focus state app_state.ui.focus_outside_canvas = false; - app_state.focused_button_index= 0; + app_state.focused_button_index = 0; "Returned to main menu".to_string() } /// Validates input, shows loading, and spawns the login task. pub fn initiate_login( - login_state: &LoginState, + login_state: &LoginFormState, app_state: &mut AppState, mut auth_client: AuthClient, sender: mpsc::Sender, ) -> String { - let username = login_state.username.clone(); - let password = login_state.password.clone(); + let username = login_state.username().to_string(); + let password = login_state.password().to_string(); - // 1. Client-side validation if username.trim().is_empty() { app_state.show_dialog( "Login Failed", @@ -163,25 +149,20 @@ pub fn initiate_login( ); "Username cannot be empty.".to_string() } else { - // 2. Show Loading Dialog app_state.show_loading_dialog("Logging In", "Please wait..."); - // 3. Spawn the login task spawn(async move { - // Use the passed-in (and moved) auth_client directly let login_outcome = match auth_client.login(username.clone(), password).await .with_context(|| format!("Spawned login task failed for identifier: {}", username)) - { - Ok(response) => LoginResult::Success(response), - Err(e) => LoginResult::Failure(format!("{}", e)), - }; - // Send result back to the main UI thread + { + Ok(response) => LoginResult::Success(response), + Err(e) => LoginResult::Failure(format!("{}", e)), + }; if let Err(e) = sender.send(login_outcome).await { error!("Failed to send login result: {}", e); } }); - // 4. Return immediately "Login initiated.".to_string() } } @@ -192,7 +173,7 @@ pub fn handle_login_result( result: LoginResult, app_state: &mut AppState, auth_state: &mut AuthState, - login_state: &mut LoginState, + login_state: &mut LoginFormState, ) -> bool { match result { LoginResult::Success(response) => { @@ -201,19 +182,15 @@ pub fn handle_login_result( auth_state.role = Some(response.role.clone()); auth_state.decoded_username = Some(response.username.clone()); - // --- NEW: Save auth data to file --- let data_to_store = StoredAuthData { access_token: response.access_token.clone(), user_id: response.user_id.clone(), role: response.role.clone(), username: response.username.clone(), }; - if let Err(e) = save_auth_data(&data_to_store) { error!("Failed to save auth data to file: {}", e); - // Continue anyway - user is still logged in for this session } - // --- END NEW --- let success_message = format!( "Login Successful!\n\nUsername: {}\nUser ID: {}\nRole: {}", @@ -227,26 +204,28 @@ pub fn handle_login_result( info!(message = %success_message, "Login successful"); } LoginResult::Failure(err_msg) | LoginResult::ConnectionError(err_msg) => { - app_state.update_dialog_content(&err_msg, vec!["OK".to_string()], DialogPurpose::LoginFailed); - login_state.error_message = Some(err_msg.clone()); + app_state.update_dialog_content( + &err_msg, + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); + login_state.set_error_message(Some(err_msg.clone())); error!(error = %err_msg, "Login failed/connection error"); } } - login_state.username.clear(); - login_state.password.clear(); + + login_state.username_mut().clear(); + login_state.password_mut().clear(); login_state.set_has_unsaved_changes(false); - login_state.current_cursor_pos = 0; - true // Request redraw as dialog content changed + login_state.set_current_cursor_pos(0); + + true } -pub async fn handle_action(action: &str,) -> Result { +pub async fn handle_action(action: &str) -> Result { match action { - "previous_entry" => { - Ok("Previous entry at tui/functions/login.rs not implemented".into()) - } - "next_entry" => { - Ok("Next entry at tui/functions/login.rs not implemented".into()) - } - _ => Err(anyhow!("Unknown login action: {}", action)) + "previous_entry" => Ok("Previous entry not implemented".into()), + "next_entry" => Ok("Next entry not implemented".into()), + _ => Err(anyhow!("Unknown login action: {}", action)), } } diff --git a/client/src/pages/login/state.rs b/client/src/pages/login/state.rs index 803fb69..d7483c5 100644 --- a/client/src/pages/login/state.rs +++ b/client/src/pages/login/state.rs @@ -1,6 +1,8 @@ // src/pages/login/state.rs use canvas::{AppMode, DataProvider}; +use canvas::FormEditor; +use std::fmt; #[derive(Debug, Clone)] pub struct LoginState { @@ -119,3 +121,114 @@ impl DataProvider for LoginState { false // Login form doesn't support suggestions } } + +/// Wrapper that owns both the raw login state and its editor + +pub struct LoginFormState { + pub state: LoginState, + pub editor: FormEditor, +} + +// manual debug because FormEditor doesnt implement debug +impl fmt::Debug for LoginFormState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LoginFormState") + .field("state", &self.state) // ✅ only print the data + .finish() + } +} + +impl LoginFormState { + /// Create a new LoginFormState with default LoginState and FormEditor + pub fn new() -> Self { + let state = LoginState::default(); + let editor = FormEditor::new(state.clone()); + Self { state, editor } + } + + // === Delegates to LoginState fields === + + pub fn username(&self) -> &str { + &self.state.username + } + + pub fn username_mut(&mut self) -> &mut String { + &mut self.state.username + } + + pub fn password(&self) -> &str { + &self.state.password + } + + pub fn password_mut(&mut self) -> &mut String { + &mut self.state.password + } + + pub fn error_message(&self) -> Option<&String> { + self.state.error_message.as_ref() + } + + pub fn set_error_message(&mut self, msg: Option) { + self.state.error_message = msg; + } + + pub fn has_unsaved_changes(&self) -> bool { + self.state.has_unsaved_changes + } + + pub fn set_has_unsaved_changes(&mut self, changed: bool) { + self.state.has_unsaved_changes = changed; + } + + pub fn clear(&mut self) { + self.state.username.clear(); + self.state.password.clear(); + self.state.error_message = None; + self.state.has_unsaved_changes = false; + self.state.login_request_pending = false; + self.state.current_cursor_pos = 0; + } + + // === Delegates to LoginState cursor/input === + + pub fn current_field(&self) -> usize { + self.state.current_field() + } + + pub fn set_current_field(&mut self, index: usize) { + self.state.set_current_field(index); + } + + pub fn current_cursor_pos(&self) -> usize { + self.state.current_cursor_pos() + } + + pub fn set_current_cursor_pos(&mut self, pos: usize) { + self.state.set_current_cursor_pos(pos); + } + + pub fn get_current_input(&self) -> &str { + self.state.get_current_input() + } + + pub fn get_current_input_mut(&mut self) -> &mut String { + self.state.get_current_input_mut() + } + + // === Delegates to FormEditor === + + pub fn mode(&self) -> AppMode { + self.editor.mode() + } + + pub fn cursor_position(&self) -> usize { + self.editor.cursor_position() + } + + pub fn handle_key_event( + &mut self, + key_event: crossterm::event::KeyEvent, + ) -> canvas::keymap::KeyEventOutcome { + self.editor.handle_key_event(key_event) + } +} diff --git a/client/src/pages/login/ui.rs b/client/src/pages/login/ui.rs index 3311a27..9ba0f0e 100644 --- a/client/src/pages/login/ui.rs +++ b/client/src/pages/login/ui.rs @@ -16,17 +16,20 @@ use canvas::{ render_suggestions_dropdown, DefaultCanvasTheme, }; -use crate::pages::login::LoginState; + +use crate::pages::login::LoginFormState; use crate::dialog; pub fn render_login( f: &mut Frame, area: Rect, theme: &Theme, - // FIX: take &LoginState (reference), not owned - login_state: &LoginState, + login_page: &LoginFormState, app_state: &AppState, ) { + let login_state = &login_page.state; + let editor = &login_page.editor; + // Main container let block = Block::default() .borders(Borders::ALL) @@ -52,14 +55,10 @@ pub fn render_login( ]) .split(inner_area); - // Wrap LoginState in FormEditor (no clone needed) - let editor = FormEditor::new(login_state.clone()); - - // Use DefaultCanvasTheme instead of app Theme let input_rect = render_canvas( f, chunks[0], - &editor, + editor, &DefaultCanvasTheme, ); @@ -134,14 +133,14 @@ pub fn render_login( ); // --- SUGGESTIONS DROPDOWN (if active) --- - if editor.mode() == canvas::AppMode::Edit { + if editor.mode() == canvas::AppMode::Edit { if let Some(input_rect) = input_rect { render_suggestions_dropdown( f, - f.area(), + chunks[0], input_rect, &DefaultCanvasTheme, - &editor, // FIX: pass &editor + editor, ); } } diff --git a/client/src/pages/routing/router.rs b/client/src/pages/routing/router.rs index 4cf2c82..8aef1b2 100644 --- a/client/src/pages/routing/router.rs +++ b/client/src/pages/routing/router.rs @@ -6,14 +6,14 @@ use crate::state::pages::{ }; use crate::pages::admin::AdminState; use crate::pages::forms::FormState; -use crate::pages::login::LoginState; +use crate::pages::login::LoginFormState; use crate::pages::register::RegisterState; use crate::pages::intro::IntroState; #[derive(Debug)] pub enum Page { Intro(IntroState), - Login(LoginState), + Login(LoginFormState), Register(RegisterState), Admin(AdminState), AddLogic(AddLogicState), diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index 6438dd0..a3b50ee 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -76,11 +76,11 @@ pub fn render_ui( // Page rendering is now fully router-driven match &mut router.current { Page::Intro(state) => render_intro(f, state, main_content_area, theme), - Page::Login(state) => render_login( + Page::Login(page) => render_login( f, main_content_area, theme, - state, + page, app_state, ), Page::Register(state) => render_register( diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 299b826..e9800a0 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -10,6 +10,7 @@ use crate::modes::handlers::event::{EventHandler, EventOutcome}; use crate::modes::handlers::mode_manager::{AppMode, ModeManager}; use crate::state::pages::auth::AuthState; use crate::pages::register::RegisterState; +use crate::pages::login::LoginFormState; use crate::pages::admin::AdminState; use crate::pages::admin::AdminFocus; use crate::pages::intro::IntroState; @@ -28,6 +29,7 @@ use crate::pages::register::RegisterResult; use crate::ui::handlers::context::DialogPurpose; use crate::utils::columns::filter_user_columns; use canvas::keymap::KeyEventOutcome; +use canvas::FormEditor; use anyhow::{Context, Result}; use crossterm::cursor::{SetCursorStyle, MoveTo}; use crossterm::event as crossterm_event; @@ -66,7 +68,7 @@ pub async fn run_ui() -> Result<()> { let event_reader = EventReader::new(); let mut auth_state = AuthState::default(); - let mut login_state = LoginState::default(); + let mut login_state = LoginFormState::new(); let mut register_state = RegisterState::default(); let mut intro_state = IntroState::default(); let mut admin_state = AdminState::default(); @@ -350,7 +352,9 @@ pub async fn run_ui() -> Result<()> { // Navigate with the up-to-date state router.navigate(Page::Intro(intro_state.clone())); } - AppView::Login => router.navigate(Page::Login(login_state.clone())), + AppView::Login => { + router.navigate(Page::Login(LoginFormState::new())) + } AppView::Register => router.navigate(Page::Register(register_state.clone())), AppView::Admin => { if let Page::Admin(current) = &router.current { @@ -619,8 +623,7 @@ pub async fn run_ui() -> Result<()> { let current_input = state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; - state.current_cursor_pos = - event_handler.ideal_cursor_column.min(max_cursor_pos); + state.set_current_cursor_pos(event_handler.ideal_cursor_column.min(max_cursor_pos)); } }