From d711f4c491e08096ce62b0797a742e2d8bbc2963 Mon Sep 17 00:00:00 2001 From: Priec Date: Wed, 30 Jul 2025 11:14:05 +0200 Subject: [PATCH] usage of canvas lib for auth BROKEN --- client/canvas_config.toml | 1 + client/config.toml | 3 +- client/src/components/auth/login.rs | 26 ++- client/src/components/auth/register.rs | 136 +++++++++++--- client/src/state/pages/auth.rs | 251 ++++++++++--------------- 5 files changed, 215 insertions(+), 202 deletions(-) diff --git a/client/canvas_config.toml b/client/canvas_config.toml index 3006cd5..6cb6c61 100644 --- a/client/canvas_config.toml +++ b/client/canvas_config.toml @@ -49,6 +49,7 @@ suggestion_up = ["Up", "Ctrl+p"] suggestion_down = ["Down", "Ctrl+n"] select_suggestion = ["Enter", "Tab"] exit_suggestions = ["Esc"] +trigger_autocomplete = ["Tab"] # Global keybindings (work in both modes) [keybindings.global] diff --git a/client/config.toml b/client/config.toml index 3f1f213..d889e2b 100644 --- a/client/config.toml +++ b/client/config.toml @@ -69,11 +69,10 @@ prev_field = ["shift+enter"] exit = ["esc", "ctrl+e"] delete_char_forward = ["delete"] delete_char_backward = ["backspace"] -move_left = [""] +move_left = ["left"] move_right = ["right"] suggestion_down = ["ctrl+n", "tab"] suggestion_up = ["ctrl+p", "shift+tab"] -trigger_autocomplete = ["left"] [keybindings.command] exit_command_mode = ["ctrl+g", "esc"] diff --git a/client/src/components/auth/login.rs b/client/src/components/auth/login.rs index 5108812..f9d6e7e 100644 --- a/client/src/components/auth/login.rs +++ b/client/src/components/auth/login.rs @@ -5,6 +5,7 @@ use crate::{ state::pages::auth::LoginState, components::common::dialog, state::app::state::AppState, + components::handlers::canvas_bridge::render_canvas_form, // Use our bridge function }; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect, Margin}, @@ -48,14 +49,11 @@ pub fn render_login( ]) .split(inner_area); - // --- FORM RENDERING --- - crate::components::handlers::canvas::render_canvas( + // --- FORM RENDERING (Using bridge function) --- + render_canvas_form( f, chunks[0], - login_state, - &["Username/Email", "Password"], - &login_state.current_field, - &[&login_state.username, &login_state.password], + login_state, // LoginState implements CanvasState theme, is_edit_mode, highlight_state, @@ -71,7 +69,7 @@ pub fn render_login( ); } - // --- BUTTONS --- + // --- BUTTONS (unchanged) --- let button_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) @@ -83,7 +81,7 @@ pub fn render_login( app_state.focused_button_index== login_button_index } else { false - }; + }; let mut login_style = Style::default().fg(theme.fg); let mut login_border = Style::default().fg(theme.border); if login_active { @@ -105,12 +103,12 @@ pub fn render_login( ); // Return Button - let return_button_index = 1; // Assuming Return is the second general element + 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 // Not active if focus is in canvas or other modes - }; + false + }; let mut return_style = Style::default().fg(theme.fg); let mut return_border = Style::default().fg(theme.border); if return_active { @@ -132,17 +130,15 @@ pub fn render_login( ); // --- DIALOG --- - // Check the correct field name for showing the dialog if app_state.ui.dialog.dialog_show { - // Pass all 7 arguments correctly dialog::render_dialog( f, f.area(), theme, &app_state.ui.dialog.dialog_title, &app_state.ui.dialog.dialog_message, - &app_state.ui.dialog.dialog_buttons, // Pass buttons slice - app_state.ui.dialog.dialog_active_button_index, + &app_state.ui.dialog.dialog_buttons, + app_state.ui.dialog.dialog_active_button_index, app_state.ui.dialog.is_loading, ); } diff --git a/client/src/components/auth/register.rs b/client/src/components/auth/register.rs index 4d80161..ca8faa2 100644 --- a/client/src/components/auth/register.rs +++ b/client/src/components/auth/register.rs @@ -2,16 +2,16 @@ use crate::{ config::colors::themes::Theme, - state::pages::auth::RegisterState, // Use RegisterState - components::common::{dialog, autocomplete}, + state::pages::auth::RegisterState, + components::common::dialog, state::app::state::AppState, - state::pages::canvas_state::CanvasState, modes::handlers::mode_manager::AppMode, + components::handlers::canvas_bridge::render_canvas_form, // Use our bridge function }; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect, Margin}, style::{Style, Modifier, Color}, - widgets::{Block, BorderType, Borders, Paragraph}, + widgets::{Block, BorderType, Borders, Paragraph, List, ListItem, ListState}, Frame, }; use crate::state::app::highlight::HighlightState; @@ -20,7 +20,7 @@ pub fn render_register( f: &mut Frame, area: Rect, theme: &Theme, - state: &RegisterState, // Use RegisterState + state: &RegisterState, app_state: &AppState, is_edit_mode: bool, highlight_state: &HighlightState, @@ -29,7 +29,7 @@ pub fn render_register( .borders(Borders::ALL) .border_type(BorderType::Plain) .border_style(Style::default().fg(theme.border)) - .title(" Register ") // Update title + .title(" Register ") .style(Style::default().bg(theme.bg)); f.render_widget(block, area); @@ -39,7 +39,6 @@ pub fn render_register( vertical: 1, }); - // Adjust constraints for 4 fields + error + buttons let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -50,20 +49,11 @@ pub fn render_register( ]) .split(inner_area); - // --- FORM RENDERING (Using render_canvas) --- - let active_field_rect = crate::components::handlers::canvas::render_canvas( + // --- FORM RENDERING (Using bridge function) --- + let input_rect = render_canvas_form( f, - chunks[0], // Area for the canvas - state, // The state object (RegisterState) - &[ // Field labels - "Username", - "Email*", - "Password*", - "Confirm Password", - "Role* (Tab)", - ], - &state.current_field(), // Pass current field index - &state.inputs().iter().map(|s| *s).collect::>(), // Pass inputs directly + chunks[0], + state, // RegisterState implements CanvasState theme, is_edit_mode, highlight_state, @@ -75,7 +65,6 @@ pub fn render_register( .alignment(Alignment::Center); f.render_widget(help_text, chunks[1]); - // --- ERROR MESSAGE --- if let Some(err) = &state.error_message { f.render_widget( @@ -107,7 +96,7 @@ pub fn render_register( } f.render_widget( - Paragraph::new("Register") // Update button text + Paragraph::new("Register") .style(register_style) .alignment(Alignment::Center) .block( @@ -119,7 +108,7 @@ pub fn render_register( button_chunks[0], ); - // Return Button (logic remains similar) + // 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 @@ -146,19 +135,18 @@ pub fn render_register( button_chunks[1], ); - // --- Render Autocomplete Dropdown (Draw AFTER buttons) --- + // --- AUTOCOMPLETE DROPDOWN (Simple bridge implementation) --- if app_state.current_mode == AppMode::Edit { - if let Some(suggestions) = state.get_suggestions() { - let selected = state.get_selected_suggestion_index(); - if !suggestions.is_empty() { - if let Some(input_rect) = active_field_rect { - autocomplete::render_autocomplete_dropdown(f, input_rect, f.area(), theme, suggestions, selected); - } + if let Some(autocomplete_state) = state.autocomplete_state() { + if autocomplete_state.is_active && !autocomplete_state.suggestions.is_empty() { + if let Some(field_rect) = input_rect { + render_simple_autocomplete_dropdown(f, field_rect, f.area(), theme, autocomplete_state); + } } } } - // --- DIALOG --- (Keep dialog logic) + // --- DIALOG --- if app_state.ui.dialog.dialog_show { dialog::render_dialog( f, @@ -172,3 +160,89 @@ pub fn render_register( ); } } + +/// Simple autocomplete dropdown renderer (bridge implementation) +fn render_simple_autocomplete_dropdown( + f: &mut Frame, + input_rect: Rect, + frame_area: Rect, + theme: &Theme, + autocomplete_state: &canvas::AutocompleteState, +) { + if autocomplete_state.is_loading { + // Show loading indicator + let loading_area = Rect { + x: input_rect.x, + y: input_rect.y + 1, + width: input_rect.width, + height: 3, + }; + + let loading_paragraph = Paragraph::new("Loading suggestions...") + .style(Style::default().fg(theme.fg)) + .block( + Block::default() + .borders(ratatui::widgets::Borders::ALL) + .border_style(Style::default().fg(theme.accent)) + .style(Style::default().bg(theme.bg)), + ); + + f.render_widget(loading_paragraph, loading_area); + return; + } + + if autocomplete_state.suggestions.is_empty() { + return; + } + + // Calculate dropdown position + let dropdown_height = (autocomplete_state.suggestions.len() as u16).min(8) + 2; + let dropdown_width = input_rect.width.max(20); + + let mut dropdown_area = Rect { + x: input_rect.x, + y: input_rect.y + 1, + width: dropdown_width, + height: dropdown_height, + }; + + // Keep dropdown within bounds + if dropdown_area.bottom() > frame_area.height { + dropdown_area.y = input_rect.y.saturating_sub(dropdown_height); + } + if dropdown_area.right() > frame_area.width { + dropdown_area.x = frame_area.width.saturating_sub(dropdown_width); + } + + // Create list items + let items: Vec = autocomplete_state + .suggestions + .iter() + .enumerate() + .map(|(i, suggestion)| { + let is_selected = autocomplete_state.selected_index == Some(i); + let style = if is_selected { + Style::default() + .fg(theme.bg) + .bg(theme.highlight) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.fg).bg(theme.bg) + }; + + ListItem::new(suggestion.display_text.as_str()).style(style) + }) + .collect(); + + let list = List::new(items).block( + Block::default() + .borders(ratatui::widgets::Borders::ALL) + .border_style(Style::default().fg(theme.accent)) + .style(Style::default().bg(theme.bg)), + ); + + let mut list_state = ListState::default(); + list_state.select(autocomplete_state.selected_index); + + f.render_stateful_widget(list, dropdown_area, &mut list_state); +} diff --git a/client/src/state/pages/auth.rs b/client/src/state/pages/auth.rs index bc218d5..fb2a94a 100644 --- a/client/src/state/pages/auth.rs +++ b/client/src/state/pages/auth.rs @@ -1,5 +1,6 @@ // src/state/pages/auth.rs -use crate::state::pages::canvas_state::CanvasState; +use canvas::{CanvasState, ActionContext, CanvasAction}; // Import from external library +use canvas::{AutocompleteCanvasState, AutocompleteState, SuggestionItem}; // For autocomplete use lazy_static::lazy_static; lazy_static! { @@ -44,91 +45,50 @@ pub struct RegisterState { pub current_field: usize, pub current_cursor_pos: usize, pub has_unsaved_changes: bool, - pub show_role_suggestions: bool, - pub role_suggestions: Vec, - pub selected_suggestion_index: Option, - pub in_suggestion_mode: bool, + + // NEW: Replace old autocomplete with external library's system + pub autocomplete: AutocompleteState, } impl AuthState { - /// Creates a new empty AuthState (unauthenticated) pub fn new() -> Self { - Self { - auth_token: None, - user_id: None, - role: None, - decoded_username: None, - } + Self::default() } } impl LoginState { - /// Creates a new empty LoginState pub fn new() -> Self { - Self { - username: String::new(), - password: String::new(), - error_message: None, - current_field: 0, - current_cursor_pos: 0, - has_unsaved_changes: false, - login_request_pending: false, - } + Self::default() } } impl RegisterState { - /// Creates a new empty RegisterState pub fn new() -> Self { Self { - username: String::new(), - email: String::new(), - password: String::new(), - password_confirmation: String::new(), - role: String::new(), - error_message: None, - current_field: 0, - current_cursor_pos: 0, - has_unsaved_changes: false, - show_role_suggestions: false, - role_suggestions: Vec::new(), - selected_suggestion_index: None, - in_suggestion_mode: false, + autocomplete: AutocompleteState::new(), + ..Default::default() } } - - /// Updates role suggestions based on current input - pub fn update_role_suggestions(&mut self) { - let current_input = self.role.to_lowercase(); - self.role_suggestions = AVAILABLE_ROLES - .iter() - .filter(|role| role.to_lowercase().contains(¤t_input)) - .cloned() - .collect(); - self.show_role_suggestions = !self.role_suggestions.is_empty(); - } } +// Implement external library's CanvasState for LoginState impl CanvasState for LoginState { fn current_field(&self) -> usize { self.current_field } fn current_cursor_pos(&self) -> usize { - let len = match self.current_field { - 0 => self.username.len(), - 1 => self.password.len(), - _ => 0, - }; - self.current_cursor_pos.min(len) + self.current_cursor_pos } - fn has_unsaved_changes(&self) -> bool { - self.has_unsaved_changes + fn set_current_field(&mut self, index: usize) { + if index < 2 { + self.current_field = index; + } } - fn inputs(&self) -> Vec<&String> { - vec![&self.username, &self.password] + fn set_current_cursor_pos(&mut self, pos: usize) { + self.current_cursor_pos = pos; } fn get_current_input(&self) -> &str { @@ -147,73 +107,55 @@ impl CanvasState for LoginState { } } + fn inputs(&self) -> Vec<&String> { + vec![&self.username, &self.password] + } + fn fields(&self) -> Vec<&str> { vec!["Username/Email", "Password"] } - fn set_current_field(&mut self, index: usize) { - if index < 2 { - self.current_field = index; - let len = match self.current_field { - 0 => self.username.len(), - 1 => self.password.len(), - _ => 0, - }; - self.current_cursor_pos = self.current_cursor_pos.min(len); - } - } - - fn set_current_cursor_pos(&mut self, pos: usize) { - let len = match self.current_field { - 0 => self.username.len(), - 1 => self.password.len(), - _ => 0, - }; - self.current_cursor_pos = pos.min(len); - } - - fn set_has_unsaved_changes(&mut self, changed: bool) { - self.has_unsaved_changes = changed; - } - - fn get_suggestions(&self) -> Option<&[String]> { - None - } - - fn get_selected_suggestion_index(&self) -> Option { - None - } -} - -impl CanvasState for RegisterState { - fn current_field(&self) -> usize { - self.current_field - } - - fn current_cursor_pos(&self) -> usize { - let len = match self.current_field { - 0 => self.username.len(), - 1 => self.email.len(), - 2 => self.password.len(), - 3 => self.password_confirmation.len(), - 4 => self.role.len(), - _ => 0, - }; - self.current_cursor_pos.min(len) - } - fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } - fn inputs(&self) -> Vec<&String> { - vec![ - &self.username, - &self.email, - &self.password, - &self.password_confirmation, - &self.role, - ] + fn set_has_unsaved_changes(&mut self, changed: bool) { + self.has_unsaved_changes = changed; + } + + // Handle custom actions (like submit) + fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { + match action { + CanvasAction::Custom(action_str) if action_str == "submit" => { + if !self.username.is_empty() && !self.password.is_empty() { + Some(format!("Submitting login for: {}", self.username)) + } else { + Some("Please fill in all required fields".to_string()) + } + } + _ => None, + } + } +} + +// Implement external library's CanvasState for RegisterState +impl CanvasState for RegisterState { + fn current_field(&self) -> usize { + self.current_field + } + + fn current_cursor_pos(&self) -> usize { + self.current_cursor_pos + } + + fn set_current_field(&mut self, index: usize) { + if index < 5 { + self.current_field = index; + } + } + + fn set_current_cursor_pos(&mut self, pos: usize) { + self.current_cursor_pos = pos; } fn get_current_input(&self) -> &str { @@ -238,6 +180,16 @@ impl CanvasState for RegisterState { } } + fn inputs(&self) -> Vec<&String> { + vec![ + &self.username, + &self.email, + &self.password, + &self.password_confirmation, + &self.role, + ] + } + fn fields(&self) -> Vec<&str> { vec![ "Username", @@ -248,50 +200,41 @@ impl CanvasState for RegisterState { ] } - fn set_current_field(&mut self, index: usize) { - if index < 5 { - self.current_field = index; - let len = match self.current_field { - 0 => self.username.len(), - 1 => self.email.len(), - 2 => self.password.len(), - 3 => self.password_confirmation.len(), - 4 => self.role.len(), - _ => 0, - }; - self.current_cursor_pos = self.current_cursor_pos.min(len); - } - } - - fn set_current_cursor_pos(&mut self, pos: usize) { - let len = match self.current_field { - 0 => self.username.len(), - 1 => self.email.len(), - 2 => self.password.len(), - 3 => self.password_confirmation.len(), - 4 => self.role.len(), - _ => 0, - }; - self.current_cursor_pos = pos.min(len); + fn has_unsaved_changes(&self) -> bool { + self.has_unsaved_changes } fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_unsaved_changes = changed; } - fn get_suggestions(&self) -> Option<&[String]> { - if self.current_field == 4 && self.in_suggestion_mode && self.show_role_suggestions { - Some(&self.role_suggestions) - } else { - None - } - } - - fn get_selected_suggestion_index(&self) -> Option { - if self.current_field == 4 && self.in_suggestion_mode && self.show_role_suggestions { - self.selected_suggestion_index - } else { - None + fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { + match action { + CanvasAction::Custom(action_str) if action_str == "submit" => { + if !self.username.is_empty() { + Some(format!("Submitting registration for: {}", self.username)) + } else { + Some("Username is required".to_string()) + } + } + _ => None, } } } + +// Add autocomplete support for RegisterState +impl AutocompleteCanvasState for RegisterState { + type SuggestionData = String; + + fn supports_autocomplete(&self, field_index: usize) -> bool { + field_index == 4 // Only role field supports autocomplete + } + + fn autocomplete_state(&self) -> Option<&AutocompleteState> { + Some(&self.autocomplete) + } + + fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState> { + Some(&mut self.autocomplete) + } +}