diff --git a/client/src/modes/canvas/common_mode.rs b/client/src/modes/canvas/common_mode.rs index 834815c..6723a4f 100644 --- a/client/src/modes/canvas/common_mode.rs +++ b/client/src/modes/canvas/common_mode.rs @@ -11,7 +11,7 @@ use anyhow::{Context, Result}; use crate::tui::functions::common::{ form::{save as form_save, revert as form_revert}, login::{save as login_save, revert as login_revert}, - register::{save as register_save, revert as register_revert}, + register::{revert as register_revert}, }; pub async fn handle_core_action( @@ -32,9 +32,6 @@ pub async fn handle_core_action( if app_state.ui.show_login { let message = login_save(auth_state, login_state, auth_client, app_state).await.context("Login save action failed")?; Ok(EventOutcome::Ok(message)) - } else if app_state.ui.show_register { - let message = register_save(register_state, auth_client, app_state).await.context("Register save_and_quit action failed")?; - Ok(EventOutcome::Ok(message)) } else { let save_outcome = form_save( form_state, @@ -57,8 +54,6 @@ pub async fn handle_core_action( "save_and_quit" => { let message = if app_state.ui.show_login { login_save(auth_state, login_state, auth_client, app_state).await.context("Login save n quit action failed")? - } else if app_state.ui.show_register { - register_save(register_state, auth_client, app_state).await.context("Register save n quit action failed")? } else { let save_outcome = form_save( form_state, diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index a92cdca..49feece 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -12,11 +12,7 @@ use anyhow::{Context, Result}; use crate::tui::{ terminal::core::TerminalCore, functions::{ - common::{ - form::SaveOutcome, - login, - register, - }, + common::{form::SaveOutcome, login, register}, }, {intro, admin}, }; @@ -42,8 +38,8 @@ use crate::modes::{ }; use crate::functions::modes::navigation::{admin_nav, add_table_nav}; use crate::config::binds::key_sequences::KeySequenceTracker; -use tokio::spawn; use tokio::sync::mpsc; +use tokio::spawn; use crate::tui::functions::common::login::LoginResult; use crate::tui::functions::common::register::RegisterResult; @@ -286,50 +282,8 @@ impl EventHandler { } UiContext::Register => { let register_action_message = match index { - 0 => { // "Register" button pressed - // Clone necessary data - let username = register_state.username.clone(); - let email = register_state.email.clone(); - let password = register_state.password.clone(); - let password_confirmation = register_state.password_confirmation.clone(); - let role = register_state.role.clone(); - - // 1. Client-side validation (similar to register::save) - if username.trim().is_empty() { - app_state.show_dialog("Registration Failed", "Username cannot be empty.", vec!["OK".to_string()], DialogPurpose::RegisterFailed); - "Username cannot be empty.".to_string() - } else if !password.is_empty() && password != password_confirmation { - app_state.show_dialog("Registration Failed", "Passwords do not match.", vec!["OK".to_string()], DialogPurpose::RegisterFailed); - "Passwords do not match.".to_string() - } else { - // 2. Show Loading Dialog - app_state.show_loading_dialog("Registering", "Please wait..."); - - // 3. Clone sender for the task - let sender = self.register_result_sender.clone(); - - // 4. Spawn the registration task - spawn(async move { - let register_outcome = match AuthClient::new().await { - Ok(mut auth_client) => { - // Handle optional fields correctly for the gRPC call - let password_opt = if password.is_empty() { None } else { Some(password) }; - let password_conf_opt = if password_confirmation.is_empty() { None } else { Some(password_confirmation) }; - let role_opt = if role.is_empty() { None } else { Some(role) }; - - match auth_client.register(username.clone(), email, password_opt, password_conf_opt, role_opt).await { - Ok(response) => register::RegisterResult::Success(response), - Err(e) => register::RegisterResult::Failure(format!("{}", e)), - } - } - Err(e) => register::RegisterResult::ConnectionError(format!("Failed to create AuthClient: {}", e)), - }; - let _ = sender.send(register_outcome).await; - }); - - // 5. Return immediately - "Registration initiated.".to_string() - } + 0 => { + register::initiate_registration(register_state, app_state, self.register_result_sender.clone()) }, 1 => register::back_to_login(register_state, app_state, buffer_state).await, _ => "Invalid Login Option".to_string(), diff --git a/client/src/tui/functions/common/register.rs b/client/src/tui/functions/common/register.rs index 842bdf6..abf5bf0 100644 --- a/client/src/tui/functions/common/register.rs +++ b/client/src/tui/functions/common/register.rs @@ -9,7 +9,10 @@ use crate::state::{ use crate::ui::handlers::context::DialogPurpose; use crate::state::app::buffer::{AppView, BufferState}; use common::proto::multieko2::auth::AuthResponse; -use anyhow::Result; +use anyhow::Context; +use tokio::spawn; +use tokio::sync::mpsc; +use tracing::{info, error}; #[derive(Debug)] pub enum RegisterResult { @@ -18,109 +21,6 @@ pub enum RegisterResult { ConnectionError(String), } -/// Attempts to register the user using the provided details via gRPC. -/// Updates RegisterState and AppState on success or failure. -pub async fn save( - register_state: &mut RegisterState, - auth_client: &mut AuthClient, - app_state: &mut AppState, -) -> Result { - let username = register_state.username.clone(); - let email = register_state.email.clone(); - // Handle optional passwords: send None if empty, Some(value) otherwise - let password = if register_state.password.is_empty() { - None - } else { - Some(register_state.password.clone()) - }; - let password_confirmation = if register_state.password_confirmation.is_empty() { - None - } else { - Some(register_state.password_confirmation.clone()) - }; - let role = if register_state.role.is_empty() { - None - } else { - Some(register_state.role.clone()) - }; - - // Basic client-side validation (example) - if username.is_empty() { - app_state.show_dialog( - "Registration Failed", - "Username cannot be empty.", - vec!["OK".to_string()], - DialogPurpose::RegisterFailed, - ); - register_state.error_message = Some("Username cannot be empty.".to_string()); - return Ok("Registration failed: Username cannot be empty.".to_string()); - } - if password.is_some() && password != password_confirmation { - app_state.show_dialog( - "Registration Failed", - "Passwords do not match.", - vec!["OK".to_string()], - DialogPurpose::RegisterFailed, - ); - register_state.error_message = Some("Passwords do not match.".to_string()); - return Ok("Registration failed: Passwords do not match.".to_string()); - } - - - // Clear previous error/dialog state before attempting - register_state.error_message = None; - app_state.hide_dialog(); - - // Call the gRPC register method - match auth_client.register(username, email, password, password_confirmation, role).await { - Ok(response) => { - // Clear fields on success? Optional, maybe wait for dialog confirmation. - // register_state.username.clear(); - // register_state.email.clear(); - // register_state.password.clear(); - // register_state.password_confirmation.clear(); - register_state.set_has_unsaved_changes(false); - - let success_message = format!( - "Registration Successful!\n\n\ - User ID: {}\n\ - Username: {}\n\ - Email: {}\n\ - Role: {}", - response.id, - response.username, - response.email, - response.role - ); - - // Show success dialog - app_state.show_dialog( - "Registration Success", - &success_message, - vec!["OK".to_string()], // Simple OK for now - DialogPurpose::RegisterSuccess, - ); - - Ok("Registration successful, details shown in dialog.".to_string()) - } - Err(e) => { - let error_message = format!("{}", e); - register_state.error_message = Some(error_message.clone()); - register_state.set_has_unsaved_changes(true); // Keep changes on error - - // Show error dialog - app_state.show_dialog( - "Registration Failed", - &error_message, - vec!["OK".to_string()], - DialogPurpose::RegisterFailed, - ); - - Ok(format!("Registration failed: {}", error_message)) - } - } -} - /// Clears the registration form fields. pub async fn revert( register_state: &mut RegisterState, @@ -161,3 +61,89 @@ pub async fn back_to_login( "Returned to main menu".to_string() } +/// Validates input, shows loading, and spawns the registration task. +pub fn initiate_registration( + register_state: &RegisterState, + app_state: &mut AppState, + sender: mpsc::Sender, +) -> String { + // Clone necessary data + let username = register_state.username.clone(); + let email = register_state.email.clone(); + let password = register_state.password.clone(); + let password_confirmation = register_state.password_confirmation.clone(); + let role = register_state.role.clone(); + + // 1. Client-side validation + if username.trim().is_empty() { + app_state.show_dialog("Registration Failed", "Username cannot be empty.", vec!["OK".to_string()], DialogPurpose::RegisterFailed); + "Username cannot be empty.".to_string() + } else if !password.is_empty() && password != password_confirmation { + app_state.show_dialog("Registration Failed", "Passwords do not match.", vec!["OK".to_string()], DialogPurpose::RegisterFailed); + "Passwords do not match.".to_string() + } else { + // 2. Show Loading Dialog + app_state.show_loading_dialog("Registering", "Please wait..."); + + // 3. Spawn the registration task + spawn(async move { + let register_outcome = match AuthClient::new().await { + Ok(mut auth_client) => { + // Handle optional fields correctly for the gRPC call + let password_opt = if password.is_empty() { None } else { Some(password) }; + let password_conf_opt = if password_confirmation.is_empty() { None } else { Some(password_confirmation) }; + let role_opt = if role.is_empty() { None } else { Some(role) }; + + match auth_client.register(username.clone(), email, password_opt, password_conf_opt, role_opt).await + .with_context(|| format!("Spawned register task failed for username: {}", username)) + { + Ok(response) => RegisterResult::Success(response), + Err(e) => RegisterResult::Failure(format!("{}", e)), + } + } + Err(e) => RegisterResult::ConnectionError(format!("Failed to create AuthClient: {}", e)), + }; + // Send result back to the main UI thread + if let Err(e) = sender.send(register_outcome).await { + error!("Failed to send registration result: {}", e); + } + }); + + // 4. Return immediately + "Registration initiated.".to_string() + } +} + +/// Handles the result received from the registration task. +/// Returns true if a redraw is needed. +pub fn handle_registration_result( + result: RegisterResult, + app_state: &mut AppState, + register_state: &mut RegisterState, +) -> bool { + match result { + RegisterResult::Success(response) => { + let success_message = format!( + "Registration Successful!\n\nUser ID: {}\nUsername: {}\nEmail: {}\nRole: {}", + response.id, response.username, response.email, response.role + ); + app_state.update_dialog_content( + &success_message, + vec!["OK".to_string()], + DialogPurpose::RegisterSuccess, + ); + info!(message = %success_message, "Registration successful"); + } + RegisterResult::Failure(err_msg) | RegisterResult::ConnectionError(err_msg) => { + app_state.update_dialog_content( + &err_msg, + vec!["OK".to_string()], + DialogPurpose::RegisterFailed, + ); + register_state.error_message = Some(err_msg.clone()); + error!(error = %err_msg, "Registration failed/connection error"); + } + } + register_state.set_has_unsaved_changes(false); // Clear flag after processing + true // Request redraw as dialog content changed +} diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index f92da49..14c2b03 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -23,6 +23,7 @@ use crate::tui::terminal::{EventReader, TerminalCore}; use crate::ui::handlers::render::render_ui; use crate::tui::functions::common::login::LoginResult; use crate::tui::functions::common::register::RegisterResult; +use crate::tui::functions::common::register; use std::time::Instant; use anyhow::{Context, Result}; use crossterm::cursor::SetCursorStyle; @@ -249,43 +250,10 @@ pub async fn run_ui() -> Result<()> { // --- Check for Register Results from Channel --- match register_result_receiver.try_recv() { - Ok(register_result) => { - // A result arrived from the register task! - match register_result { - RegisterResult::Success(response) => { - // Update Dialog - let success_message = format!( - "Registration Successful!\n\nUser ID: {}\nUsername: {}\nEmail: {}\nRole: {}", - response.id, response.username, response.email, response.role - ); - app_state.update_dialog_content( // Update loading dialog - &success_message, - vec!["OK".to_string()], // Simple OK for now - DialogPurpose::RegisterSuccess, - ); - info!(message = %success_message, "Registration successful"); - } - RegisterResult::Failure(err_msg) => { - app_state.update_dialog_content( // Update loading dialog - &err_msg, - vec!["OK".to_string()], - DialogPurpose::RegisterFailed, - ); - register_state.error_message = Some(err_msg.clone()); // Keep error message - error!(error = %err_msg, "Registration failed"); - } - RegisterResult::ConnectionError(err_msg) => { - app_state.update_dialog_content( // Update loading dialog - &err_msg, // Show connection error - vec!["OK".to_string()], - DialogPurpose::RegisterFailed, // Still a failure from user perspective - ); - register_state.error_message = Some(err_msg.clone()); - error!(error = %err_msg, "Registration connection error"); - } + Ok(result) => { + if register::handle_registration_result(result, &mut app_state, &mut register_state) { + needs_redraw = true; } - register_state.set_has_unsaved_changes(false); // Clear unsaved changes flag after processing - needs_redraw = true; // Set flag: Register result processed, UI state (dialog) changed } Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ } Err(mpsc::error::TryRecvError::Disconnected) => {