From 858f5137d8dee166f9f44da3c78e799fa1cc1884 Mon Sep 17 00:00:00 2001 From: Priec Date: Tue, 19 Aug 2025 10:56:54 +0200 Subject: [PATCH] migrating to the new canvas library --- client/Cargo.toml | 2 +- client/src/state/pages/auth.rs | 295 +++++++++++++++------------------ client/src/state/pages/form.rs | 163 ++++++++---------- 3 files changed, 203 insertions(+), 257 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index 5aaffd5..ea6d4ac 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -8,7 +8,7 @@ license.workspace = true anyhow = { workspace = true } async-trait = "0.1.88" common = { path = "../common" } -canvas = { path = "../canvas", features = ["gui"] } +canvas = { path = "../canvas", features = ["gui", "suggestions"] } ratatui = { workspace = true } crossterm = { workspace = true } diff --git a/client/src/state/pages/auth.rs b/client/src/state/pages/auth.rs index dffdcf9..e4a9f05 100644 --- a/client/src/state/pages/auth.rs +++ b/client/src/state/pages/auth.rs @@ -1,6 +1,5 @@ // src/state/pages/auth.rs -use canvas::canvas::{CanvasState, ActionContext, CanvasAction, AppMode}; -use canvas::autocomplete::{AutocompleteCanvasState, AutocompleteState, SuggestionItem}; +use canvas::{DataProvider, AppMode, SuggestionItem}; use lazy_static::lazy_static; lazy_static! { @@ -60,8 +59,10 @@ pub struct RegisterState { pub current_field: usize, pub current_cursor_pos: usize, pub has_unsaved_changes: bool, - pub autocomplete: AutocompleteState, pub app_mode: AppMode, + // Keep role suggestions for later integration + pub role_suggestions: Vec, + pub role_suggestions_active: bool, } impl Default for RegisterState { @@ -76,8 +77,9 @@ impl Default for RegisterState { current_field: 0, current_cursor_pos: 0, has_unsaved_changes: false, - autocomplete: AutocompleteState::new(), app_mode: AppMode::Edit, + role_suggestions: AVAILABLE_ROLES.clone(), + role_suggestions_active: false, } } } @@ -95,51 +97,27 @@ impl LoginState { ..Default::default() } } -} -impl RegisterState { - pub fn new() -> Self { - let mut state = Self { - autocomplete: AutocompleteState::new(), - app_mode: AppMode::Edit, - ..Default::default() - }; - - // Initialize autocomplete with role suggestions - let suggestions: Vec> = AVAILABLE_ROLES - .iter() - .map(|role| SuggestionItem::simple(role.clone(), role.clone())) - .collect(); - - // Set suggestions but keep inactive initially - state.autocomplete.set_suggestions(suggestions); - state.autocomplete.is_active = false; // Not active by default - - state - } -} - -// Implement external library's CanvasState for LoginState -impl CanvasState for LoginState { - fn current_field(&self) -> usize { + // Legacy method compatibility + pub fn current_field(&self) -> usize { self.current_field } - fn current_cursor_pos(&self) -> usize { + pub fn current_cursor_pos(&self) -> usize { self.current_cursor_pos } - fn set_current_field(&mut self, index: usize) { + pub fn set_current_field(&mut self, index: usize) { if index < 2 { self.current_field = index; } } - fn set_current_cursor_pos(&mut self, pos: usize) { + pub fn set_current_cursor_pos(&mut self, pos: usize) { self.current_cursor_pos = pos; } - fn get_current_input(&self) -> &str { + pub fn get_current_input(&self) -> &str { match self.current_field { 0 => &self.username, 1 => &self.password, @@ -147,7 +125,7 @@ impl CanvasState for LoginState { } } - fn get_current_input_mut(&mut self) -> &mut String { + pub fn get_current_input_mut(&mut self) -> &mut String { match self.current_field { 0 => &mut self.username, 1 => &mut self.password, @@ -155,68 +133,57 @@ impl CanvasState for LoginState { } } - fn inputs(&self) -> Vec<&String> { - vec![&self.username, &self.password] + pub fn current_mode(&self) -> AppMode { + self.app_mode } - fn fields(&self) -> Vec<&str> { - vec!["Username/Email", "Password"] - } - - fn has_unsaved_changes(&self) -> bool { + // Add missing methods that used to come from CanvasState trait + pub fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } - fn set_has_unsaved_changes(&mut self, changed: bool) { + pub fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_unsaved_changes = changed; } - - 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, - } - } - - fn current_mode(&self) -> AppMode { - self.app_mode - } } -// Implement external library's CanvasState for RegisterState -impl CanvasState for RegisterState { - fn current_field(&self) -> usize { +impl RegisterState { + pub fn new() -> Self { + Self { + app_mode: AppMode::Edit, + role_suggestions: AVAILABLE_ROLES.clone(), + role_suggestions_active: false, + ..Default::default() + } + } + + // Legacy method compatibility + pub fn current_field(&self) -> usize { self.current_field } - fn current_cursor_pos(&self) -> usize { + pub fn current_cursor_pos(&self) -> usize { self.current_cursor_pos } - fn set_current_field(&mut self, index: usize) { + pub fn set_current_field(&mut self, index: usize) { if index < 5 { self.current_field = index; - - // Auto-activate autocomplete when moving to role field (index 4) - if index == 4 && !self.autocomplete.is_active { - self.activate_autocomplete(); - } else if index != 4 && self.autocomplete.is_active { - self.deactivate_autocomplete(); + + // Auto-activate role suggestions when moving to role field (index 4) + if index == 4 { + self.activate_role_suggestions(); + } else { + self.deactivate_role_suggestions(); } } } - fn set_current_cursor_pos(&mut self, pos: usize) { + pub fn set_current_cursor_pos(&mut self, pos: usize) { self.current_cursor_pos = pos; } - fn get_current_input(&self) -> &str { + pub fn get_current_input(&self) -> &str { match self.current_field { 0 => &self.username, 1 => &self.email, @@ -227,7 +194,7 @@ impl CanvasState for RegisterState { } } - fn get_current_input_mut(&mut self) -> &mut String { + pub fn get_current_input_mut(&mut self) -> &mut String { match self.current_field { 0 => &mut self.username, 1 => &mut self.email, @@ -238,123 +205,121 @@ impl CanvasState for RegisterState { } } - fn inputs(&self) -> Vec<&String> { - vec![ - &self.username, - &self.email, - &self.password, - &self.password_confirmation, - &self.role, - ] + pub fn current_mode(&self) -> AppMode { + self.app_mode } - fn fields(&self) -> Vec<&str> { - vec![ - "Username", - "Email (Optional)", - "Password (Optional)", - "Confirm Password", - "Role (Optional)" - ] + // Role suggestions management + pub fn activate_role_suggestions(&mut self) { + self.role_suggestions_active = true; + // Filter suggestions based on current input + let current_input = self.role.to_lowercase(); + self.role_suggestions = AVAILABLE_ROLES + .iter() + .filter(|role| role.to_lowercase().contains(¤t_input)) + .cloned() + .collect(); } - fn has_unsaved_changes(&self) -> bool { + pub fn deactivate_role_suggestions(&mut self) { + self.role_suggestions_active = false; + } + + pub fn is_role_suggestions_active(&self) -> bool { + self.role_suggestions_active + } + + pub fn get_role_suggestions(&self) -> &[String] { + &self.role_suggestions + } + + // Add missing methods that used to come from CanvasState trait + pub fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } - fn set_has_unsaved_changes(&mut self, changed: bool) { + pub fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_unsaved_changes = changed; } +} - 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, +// Step 2: Implement DataProvider for LoginState +impl DataProvider for LoginState { + fn field_count(&self) -> usize { + 2 + } + + fn field_name(&self, index: usize) -> &str { + match index { + 0 => "Username/Email", + 1 => "Password", + _ => "", } } - fn current_mode(&self) -> AppMode { - self.app_mode + fn field_value(&self, index: usize) -> &str { + match index { + 0 => &self.username, + 1 => &self.password, + _ => "", + } + } + + fn set_field_value(&mut self, index: usize, value: String) { + match index { + 0 => self.username = value, + 1 => self.password = value, + _ => {} + } + self.has_unsaved_changes = true; + } + + fn supports_suggestions(&self, _field_index: usize) -> bool { + false // Login form doesn't support suggestions } } -// 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 +// Step 3: Implement DataProvider for RegisterState +impl DataProvider for RegisterState { + fn field_count(&self) -> usize { + 5 } - fn autocomplete_state(&self) -> Option<&AutocompleteState> { - Some(&self.autocomplete) - } - - fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState> { - Some(&mut self.autocomplete) - } - - fn activate_autocomplete(&mut self) { - let current_field = self.current_field(); - if self.supports_autocomplete(current_field) { - self.autocomplete.activate(current_field); - - // Re-filter suggestions based on current input - let current_input = self.role.to_lowercase(); - let filtered_suggestions: Vec> = AVAILABLE_ROLES - .iter() - .filter(|role| role.to_lowercase().contains(¤t_input)) - .map(|role| SuggestionItem::simple(role.clone(), role.clone())) - .collect(); - - self.autocomplete.set_suggestions(filtered_suggestions); + fn field_name(&self, index: usize) -> &str { + match index { + 0 => "Username", + 1 => "Email (Optional)", + 2 => "Password (Optional)", + 3 => "Confirm Password", + 4 => "Role (Optional)", + _ => "", } } - fn deactivate_autocomplete(&mut self) { - self.autocomplete.deactivate(); - } - - fn is_autocomplete_active(&self) -> bool { - self.autocomplete.is_active - } - - fn is_autocomplete_ready(&self) -> bool { - self.autocomplete.is_ready() - } - - fn apply_autocomplete_selection(&mut self) -> Option { - // First, get the data we need and clone it to avoid borrowing conflicts - let selection_info = self.autocomplete.get_selected().map(|selected| { - (selected.value_to_store.clone(), selected.display_text.clone()) - }); - - // Now do the mutable operations - if let Some((value, display_text)) = selection_info { - self.role = value; - self.set_has_unsaved_changes(true); - self.deactivate_autocomplete(); - Some(format!("Selected role: {}", display_text)) - } else { - None + fn field_value(&self, index: usize) -> &str { + match index { + 0 => &self.username, + 1 => &self.email, + 2 => &self.password, + 3 => &self.password_confirmation, + 4 => &self.role, + _ => "", } } - fn set_autocomplete_suggestions(&mut self, suggestions: Vec>) { - if let Some(state) = self.autocomplete_state_mut() { - state.set_suggestions(suggestions); + fn set_field_value(&mut self, index: usize, value: String) { + match index { + 0 => self.username = value, + 1 => self.email = value, + 2 => self.password = value, + 3 => self.password_confirmation = value, + 4 => self.role = value, + _ => {} } + self.has_unsaved_changes = true; } - fn set_autocomplete_loading(&mut self, loading: bool) { - if let Some(state) = self.autocomplete_state_mut() { - state.is_loading = loading; - } + fn supports_suggestions(&self, field_index: usize) -> bool { + field_index == 4 // only Role field supports suggestions } } diff --git a/client/src/state/pages/form.rs b/client/src/state/pages/form.rs index 93dc480..8df94d7 100644 --- a/client/src/state/pages/form.rs +++ b/client/src/state/pages/form.rs @@ -1,7 +1,8 @@ // src/state/pages/form.rs use crate::config::colors::themes::Theme; -use canvas::canvas::{CanvasState, CanvasAction, ActionContext, HighlightState, AppMode}; +use canvas::{DataProvider, AppMode, EditorState, FormEditor}; +use canvas::canvas::HighlightState; use common::proto::komp_ac::search::search_response::Hit; use ratatui::layout::Rect; use ratatui::Frame; @@ -122,26 +123,19 @@ impl FormState { area: Rect, theme: &Theme, is_edit_mode: bool, - highlight_state: &HighlightState, // Now using canvas::HighlightState + highlight_state: &HighlightState, ) { - let fields_str_slice: Vec<&str> = - self.fields().iter().map(|s| *s).collect(); - let values_str_slice: Vec<&String> = self.values.iter().collect(); - - crate::components::form::form::render_form( - f, - area, - self, - &fields_str_slice, - &self.current_field, - &values_str_slice, - &self.table_name, - theme, - is_edit_mode, - highlight_state, - self.total_count, - self.current_position, - ); + // Wrap in FormEditor for new API + let mut editor = FormEditor::new(self.clone()); + + // Use new canvas rendering + canvas::render_canvas_default(f, area, &editor); + + // If autocomplete is active, render suggestions + if self.autocomplete_active && !self.autocomplete_suggestions.is_empty() { + // Note: This will need to be updated when suggestions are integrated + // canvas::render_suggestions_dropdown(f, area, input_rect, &canvas::DefaultCanvasTheme, &editor); + } } pub fn reset_to_empty(&mut self) { @@ -242,97 +236,84 @@ impl FormState { pub fn set_readonly_mode(&mut self) { self.app_mode = AppMode::ReadOnly; } -} -impl CanvasState for FormState { - fn current_field(&self) -> usize { - self.current_field - } - - fn current_cursor_pos(&self) -> usize { - self.current_cursor_pos - } - - fn has_unsaved_changes(&self) -> bool { - self.has_unsaved_changes - } - - fn inputs(&self) -> Vec<&String> { - self.values.iter().collect() - } - - fn get_current_input(&self) -> &str { - FormState::get_current_input(self) - } - - fn get_current_input_mut(&mut self) -> &mut String { - FormState::get_current_input_mut(self) - } - - fn fields(&self) -> Vec<&str> { + // Legacy method compatibility + pub fn fields(&self) -> Vec<&str> { self.fields .iter() .map(|f| f.display_name.as_str()) .collect() } - fn set_current_field(&mut self, index: usize) { + pub fn get_display_value_for_field(&self, index: usize) -> &str { + if let Some(display_text) = self.link_display_map.get(&index) { + return display_text.as_str(); + } + self.values + .get(index) + .map(|s| s.as_str()) + .unwrap_or("") + } + + pub fn has_display_override(&self, index: usize) -> bool { + self.link_display_map.contains_key(&index) + } + + pub fn current_mode(&self) -> AppMode { + self.app_mode + } + + // Add missing methods that used to come from CanvasState trait + pub fn has_unsaved_changes(&self) -> bool { + self.has_unsaved_changes + } + + pub fn set_has_unsaved_changes(&mut self, changed: bool) { + self.has_unsaved_changes = changed; + } + + pub fn current_field(&self) -> usize { + self.current_field + } + + pub fn set_current_field(&mut self, index: usize) { if index < self.fields.len() { self.current_field = index; } self.deactivate_autocomplete(); } - fn set_current_cursor_pos(&mut self, pos: usize) { + pub fn current_cursor_pos(&self) -> usize { + self.current_cursor_pos + } + + pub fn set_current_cursor_pos(&mut self, pos: usize) { self.current_cursor_pos = pos; } +} - fn set_has_unsaved_changes(&mut self, changed: bool) { - self.has_unsaved_changes = changed; +// Step 2: Implement DataProvider for FormState +impl DataProvider for FormState { + fn field_count(&self) -> usize { + self.fields.len() } - // --- FEATURE-SPECIFIC ACTION HANDLING --- - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - match action { - CanvasAction::SelectSuggestion => { - if let Some(selected_idx) = self.selected_suggestion_index { - if let Some(hit) = self.autocomplete_suggestions.get(selected_idx).cloned() { - // Extract the value from the selected suggestion - if let Ok(content_map) = serde_json::from_str::>(&hit.content_json) { - let current_field_def = &self.fields[self.current_field]; - if let Some(value) = content_map.get(¤t_field_def.data_key) { - let new_value = json_value_to_string(value); - let display_name = self.get_display_name_for_hit(&hit); - *self.get_current_input_mut() = new_value.clone(); - self.set_current_cursor_pos(new_value.len()); - self.set_has_unsaved_changes(true); - self.deactivate_autocomplete(); - return Some(format!("Selected: {}", display_name)); - } - } - } - } - None - } - _ => None, // Let canvas handle other actions + fn field_name(&self, index: usize) -> &str { + &self.fields[index].display_name + } + + fn field_value(&self, index: usize) -> &str { + &self.values[index] + } + + fn set_field_value(&mut self, index: usize, value: String) { + if let Some(v) = self.values.get_mut(index) { + *v = value; + self.has_unsaved_changes = true; } } - fn get_display_value_for_field(&self, index: usize) -> &str { - if let Some(display_text) = self.link_display_map.get(&index) { - return display_text.as_str(); - } - self.inputs() - .get(index) - .map(|s| s.as_str()) - .unwrap_or("") - } - - fn has_display_override(&self, index: usize) -> bool { - self.link_display_map.contains_key(&index) - } - - fn current_mode(&self) -> AppMode { - self.app_mode + fn supports_suggestions(&self, field_index: usize) -> bool { + self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false) } }