From 72c2691a1734e7bf8a4e25716b13c1bfc95fc894 Mon Sep 17 00:00:00 2001 From: filipriec Date: Fri, 29 Aug 2025 12:22:25 +0200 Subject: [PATCH] registration now has working form --- client/src/modes/general/navigation.rs | 2 +- client/src/modes/handlers/event.rs | 38 ++++++++ client/src/pages/register/logic.rs | 72 ++++++++------ client/src/pages/register/state.rs | 128 ++++++++++++++++++++++++- client/src/pages/register/ui.rs | 44 ++++----- client/src/pages/routing/router.rs | 4 +- client/src/ui/handlers/ui.rs | 18 +++- 7 files changed, 241 insertions(+), 65 deletions(-) diff --git a/client/src/modes/general/navigation.rs b/client/src/modes/general/navigation.rs index 3cf3cb1..e30195c 100644 --- a/client/src/modes/general/navigation.rs +++ b/client/src/modes/general/navigation.rs @@ -100,7 +100,7 @@ pub fn up(app_state: &mut AppState, router: &mut Router) { Page::Register(state) 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); + let last_field_index = state.state.field_count().saturating_sub(1); state.set_current_field(last_field_index); } else { app_state.focused_button_index = diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 4f75eb4..11f12cb 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -330,6 +330,44 @@ impl EventHandler { } } } + } else if let Page::Register(register_page) = &mut router.current { + use crossterm::event::{KeyCode, KeyModifiers}; + + // Inside canvas: at the last field, 'j' or Down moves focus to buttons + if !app_state.ui.focus_outside_canvas { + let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1); + let at_last = register_page.editor.current_field() >= last_idx; + if at_last + && matches!( + (key_code, modifiers), + (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) + ) + { + app_state.ui.focus_outside_canvas = true; + app_state.focused_button_index = 0; // focus "Register" button + register_page.editor.set_mode(CanvasMode::ReadOnly); + return Ok(EventOutcome::Ok("Focus moved to buttons".to_string())); + } + } + + // Only forward to the canvas while focus is inside it + if !app_state.ui.focus_outside_canvas { + match register_page.handle_key_event(key_event) { + KeyEventOutcome::Consumed(Some(msg)) => { + self.command_message = msg; + return Ok(EventOutcome::Ok("Register input updated".to_string())); + } + KeyEventOutcome::Consumed(None) => { + return Ok(EventOutcome::Ok("Register input updated".to_string())); + } + KeyEventOutcome::Pending => { + return Ok(EventOutcome::Ok("Waiting for next key...".to_string())); + } + KeyEventOutcome::NotMatched => { + // fall through + } + } + } } } if toggle_sidebar( diff --git a/client/src/pages/register/logic.rs b/client/src/pages/register/logic.rs index b1c7821..a62f4d7 100644 --- a/client/src/pages/register/logic.rs +++ b/client/src/pages/register/logic.rs @@ -1,13 +1,11 @@ // src/pages/register/logic.rs use crate::services::auth::AuthClient; -use crate::state::{ - app::state::AppState, -}; +use crate::state::app::state::AppState; use crate::ui::handlers::context::DialogPurpose; use crate::buffer::state::{AppView, BufferState}; use common::proto::komp_ac::auth::AuthResponse; -use crate::pages::register::RegisterState; +use crate::pages::register::RegisterFormState; use anyhow::Context; use tokio::spawn; use tokio::sync::mpsc; @@ -22,24 +20,26 @@ pub enum RegisterResult { /// Clears the registration form fields. pub async fn revert( - register_state: &mut RegisterState, - _app_state: &mut AppState, // Keep signature consistent if needed elsewhere + register_state: &mut RegisterFormState, + app_state: &mut AppState, ) -> String { - register_state.username.clear(); - register_state.email.clear(); - register_state.password.clear(); - register_state.password_confirmation.clear(); - register_state.role.clear(); - register_state.error_message = None; + register_state.username_mut().clear(); + register_state.email_mut().clear(); + register_state.password_mut().clear(); + register_state.password_confirmation_mut().clear(); + register_state.role_mut().clear(); + register_state.set_error_message(None); register_state.set_has_unsaved_changes(false); - register_state.current_field = 0; // Reset focus to first field - register_state.current_cursor_pos = 0; + register_state.set_current_field(0); // Reset focus to first field + register_state.set_current_cursor_pos(0); + + app_state.hide_dialog(); "Registration form cleared".to_string() } /// Clears the form and returns to the intro screen. pub async fn back_to_login( - register_state: &mut RegisterState, + register_state: &mut RegisterFormState, app_state: &mut AppState, buffer_state: &mut BufferState, ) -> String { @@ -62,25 +62,34 @@ pub async fn back_to_login( /// Validates input, shows loading, and spawns the registration task. pub fn initiate_registration( - register_state: &RegisterState, + register_state: &RegisterFormState, app_state: &mut AppState, mut auth_client: AuthClient, 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(); + let username = register_state.username().to_string(); + let email = register_state.email().to_string(); + let password = register_state.password().to_string(); + let password_confirmation = register_state.password_confirmation().to_string(); + let role = register_state.role().to_string(); // 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); + 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() + 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..."); @@ -88,14 +97,19 @@ pub fn initiate_registration( // 3. Spawn the registration task spawn(async move { 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 password_conf_opt = + if password_confirmation.is_empty() { None } else { Some(password_confirmation) }; let role_opt = if role.is_empty() { None } else { Some(role) }; - let register_outcome = match auth_client.register(username.clone(), email, password_opt, password_conf_opt, role_opt).await + + let register_outcome = 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)), }; + // Send result back to the main UI thread if let Err(e) = sender.send(register_outcome).await { error!("Failed to send registration result: {}", e); @@ -112,7 +126,7 @@ pub fn initiate_registration( pub fn handle_registration_result( result: RegisterResult, app_state: &mut AppState, - register_state: &mut RegisterState, + register_state: &mut RegisterFormState, ) -> bool { match result { RegisterResult::Success(response) => { @@ -133,7 +147,7 @@ pub fn handle_registration_result( vec!["OK".to_string()], DialogPurpose::RegisterFailed, ); - register_state.error_message = Some(err_msg.clone()); + register_state.set_error_message(Some(err_msg.clone())); error!(error = %err_msg, "Registration failed/connection error"); } } diff --git a/client/src/pages/register/state.rs b/client/src/pages/register/state.rs index 559587a..39bb393 100644 --- a/client/src/pages/register/state.rs +++ b/client/src/pages/register/state.rs @@ -1,6 +1,7 @@ // src/pages/register/state.rs -use canvas::{DataProvider, AppMode}; +use canvas::{DataProvider, AppMode, FormEditor}; +use std::fmt; use lazy_static::lazy_static; lazy_static! { @@ -182,3 +183,128 @@ impl DataProvider for RegisterState { field_index == 4 // only Role field supports suggestions } } + +/// Wrapper that owns both the raw register state and its editor +pub struct RegisterFormState { + pub state: RegisterState, + pub editor: FormEditor, +} + +impl Default for RegisterFormState { + fn default() -> Self { + Self::new() + } +} + +// manual Debug because FormEditor doesn’t implement Debug +impl fmt::Debug for RegisterFormState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RegisterFormState") + .field("state", &self.state) + .finish() + } +} + +impl RegisterFormState { + pub fn new() -> Self { + let state = RegisterState::default(); + let editor = FormEditor::new(state.clone()); + Self { state, editor } + } + + // === Delegates to RegisterState === + pub fn username(&self) -> &str { + &self.state.username + } + pub fn username_mut(&mut self) -> &mut String { + &mut self.state.username + } + + pub fn email(&self) -> &str { + &self.state.email + } + pub fn email_mut(&mut self) -> &mut String { + &mut self.state.email + } + + pub fn password(&self) -> &str { + &self.state.password + } + pub fn password_mut(&mut self) -> &mut String { + &mut self.state.password + } + + pub fn password_confirmation(&self) -> &str { + &self.state.password_confirmation + } + pub fn password_confirmation_mut(&mut self) -> &mut String { + &mut self.state.password_confirmation + } + + pub fn role(&self) -> &str { + &self.state.role + } + pub fn role_mut(&mut self) -> &mut String { + &mut self.state.role + } + + 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.email.clear(); + self.state.password.clear(); + self.state.password_confirmation.clear(); + self.state.role.clear(); + self.state.error_message = None; + self.state.has_unsaved_changes = false; + self.state.current_field = 0; + self.state.current_cursor_pos = 0; + } + + // === Delegates to 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(self.state.current_field) + } + + // === Delegates to FormEditor === + pub fn mode(&self) -> canvas::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/register/ui.rs b/client/src/pages/register/ui.rs index 71b56d9..6bf4a72 100644 --- a/client/src/pages/register/ui.rs +++ b/client/src/pages/register/ui.rs @@ -3,7 +3,6 @@ use crate::{ config::colors::themes::Theme, state::app::state::AppState, - modes::handlers::mode_manager::AppMode, }; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect, Margin}, @@ -12,16 +11,20 @@ use ratatui::{ Frame, }; use crate::dialog; -use crate::pages::register::RegisterState; -use canvas::{FormEditor, render_canvas, render_suggestions_dropdown, DefaultCanvasTheme}; +use crate::pages::register::RegisterFormState; +use canvas::{render_canvas, render_suggestions_dropdown, DefaultCanvasTheme}; pub fn render_register( f: &mut Frame, area: Rect, theme: &Theme, - state: &RegisterState, + register_page: &RegisterFormState, app_state: &AppState, ) { + let state = ®ister_page.state; + let editor = ®ister_page.editor; + + // Outer block let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Plain) @@ -46,15 +49,8 @@ pub fn render_register( ]) .split(inner_area); - // Wrap RegisterState in FormEditor - let editor = FormEditor::new(state.clone()); - - let input_rect = render_canvas( - f, - chunks[0], - &editor, - theme, - ); + // ✅ Render the form canvas + let input_rect = render_canvas(f, chunks[0], editor, theme); // --- HELP TEXT --- let help_text = Paragraph::new("* are optional fields") @@ -80,11 +76,8 @@ pub fn render_register( // Register Button let register_button_index = 0; - let register_active = if app_state.ui.focus_outside_canvas { - app_state.focused_button_index == register_button_index - } else { - false - }; + let register_active = app_state.ui.focus_outside_canvas + && app_state.focused_button_index == register_button_index; let mut register_style = Style::default().fg(theme.fg); let mut register_border = Style::default().fg(theme.border); if register_active { @@ -107,11 +100,8 @@ pub fn render_register( // Return Button let return_button_index = 1; - let return_active = if app_state.ui.focus_outside_canvas { - app_state.focused_button_index == return_button_index - } else { - false - }; + let return_active = app_state.ui.focus_outside_canvas + && app_state.focused_button_index == return_button_index; let mut return_style = Style::default().fg(theme.fg); let mut return_border = Style::default().fg(theme.border); if return_active { @@ -132,15 +122,15 @@ pub fn render_register( button_chunks[1], ); - // --- AUTOCOMPLETE DROPDOWN (Using new canvas suggestions) --- + // --- AUTOCOMPLETE DROPDOWN --- if editor.mode() == canvas::AppMode::Edit { if let Some(input_rect) = input_rect { render_suggestions_dropdown( f, - f.area(), // Frame area - input_rect, // Current input field rect + f.area(), + input_rect, &DefaultCanvasTheme, - &editor, + editor, ); } } diff --git a/client/src/pages/routing/router.rs b/client/src/pages/routing/router.rs index 8aef1b2..125f293 100644 --- a/client/src/pages/routing/router.rs +++ b/client/src/pages/routing/router.rs @@ -7,14 +7,14 @@ use crate::state::pages::{ use crate::pages::admin::AdminState; use crate::pages::forms::FormState; use crate::pages::login::LoginFormState; -use crate::pages::register::RegisterState; +use crate::pages::register::RegisterFormState; use crate::pages::intro::IntroState; #[derive(Debug)] pub enum Page { Intro(IntroState), Login(LoginFormState), - Register(RegisterState), + Register(RegisterFormState), Admin(AdminState), AddLogic(AddLogicState), AddTable(AddTableState), diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 07850f0..2b40c00 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -9,8 +9,8 @@ use crate::modes::common::commands::CommandHandler; 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::register::RegisterFormState; use crate::pages::admin::AdminState; use crate::pages::admin::AdminFocus; use crate::pages::intro::IntroState; @@ -70,7 +70,8 @@ pub async fn run_ui() -> Result<()> { let mut auth_state = AuthState::default(); let mut login_state = LoginFormState::new(); login_state.editor.set_keymap(config.build_canvas_keymap()); - let mut register_state = RegisterState::default(); + let mut register_state = RegisterFormState::default(); + register_state.editor.set_keymap(config.build_canvas_keymap()); let mut intro_state = IntroState::default(); let mut admin_state = AdminState::default(); let mut router = Router::new(); @@ -387,7 +388,15 @@ pub async fn run_ui() -> Result<()> { router.navigate(Page::Login(page)); } } - AppView::Register => router.navigate(Page::Register(register_state.clone())), + AppView::Register => { + if let Page::Register(_) = &router.current { + // already on register page + } else { + let mut page = RegisterFormState::new(); + page.editor.set_keymap(config.build_canvas_keymap()); + router.navigate(Page::Register(page)); + } + } AppView::Admin => { if let Page::Admin(current) = &router.current { admin_state = current.clone(); @@ -647,8 +656,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)); } } else if let Page::Login(state) = &mut router.current { if !app_state.is_canvas_edit_mode() {