migrating to the new canvas library

This commit is contained in:
Priec
2025-08-19 10:56:54 +02:00
parent 80d5dd0761
commit 858f5137d8
3 changed files with 203 additions and 257 deletions

View File

@@ -8,7 +8,7 @@ license.workspace = true
anyhow = { workspace = true } anyhow = { workspace = true }
async-trait = "0.1.88" async-trait = "0.1.88"
common = { path = "../common" } common = { path = "../common" }
canvas = { path = "../canvas", features = ["gui"] } canvas = { path = "../canvas", features = ["gui", "suggestions"] }
ratatui = { workspace = true } ratatui = { workspace = true }
crossterm = { workspace = true } crossterm = { workspace = true }

View File

@@ -1,6 +1,5 @@
// src/state/pages/auth.rs // src/state/pages/auth.rs
use canvas::canvas::{CanvasState, ActionContext, CanvasAction, AppMode}; use canvas::{DataProvider, AppMode, SuggestionItem};
use canvas::autocomplete::{AutocompleteCanvasState, AutocompleteState, SuggestionItem};
use lazy_static::lazy_static; use lazy_static::lazy_static;
lazy_static! { lazy_static! {
@@ -60,8 +59,10 @@ pub struct RegisterState {
pub current_field: usize, pub current_field: usize,
pub current_cursor_pos: usize, pub current_cursor_pos: usize,
pub has_unsaved_changes: bool, pub has_unsaved_changes: bool,
pub autocomplete: AutocompleteState<String>,
pub app_mode: AppMode, pub app_mode: AppMode,
// Keep role suggestions for later integration
pub role_suggestions: Vec<String>,
pub role_suggestions_active: bool,
} }
impl Default for RegisterState { impl Default for RegisterState {
@@ -76,8 +77,9 @@ impl Default for RegisterState {
current_field: 0, current_field: 0,
current_cursor_pos: 0, current_cursor_pos: 0,
has_unsaved_changes: false, has_unsaved_changes: false,
autocomplete: AutocompleteState::new(),
app_mode: AppMode::Edit, app_mode: AppMode::Edit,
role_suggestions: AVAILABLE_ROLES.clone(),
role_suggestions_active: false,
} }
} }
} }
@@ -95,51 +97,27 @@ impl LoginState {
..Default::default() ..Default::default()
} }
} }
}
impl RegisterState { // Legacy method compatibility
pub fn new() -> Self { pub fn current_field(&self) -> usize {
let mut state = Self {
autocomplete: AutocompleteState::new(),
app_mode: AppMode::Edit,
..Default::default()
};
// Initialize autocomplete with role suggestions
let suggestions: Vec<SuggestionItem<String>> = 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 {
self.current_field self.current_field
} }
fn current_cursor_pos(&self) -> usize { pub fn current_cursor_pos(&self) -> usize {
self.current_cursor_pos self.current_cursor_pos
} }
fn set_current_field(&mut self, index: usize) { pub fn set_current_field(&mut self, index: usize) {
if index < 2 { if index < 2 {
self.current_field = index; 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; self.current_cursor_pos = pos;
} }
fn get_current_input(&self) -> &str { pub fn get_current_input(&self) -> &str {
match self.current_field { match self.current_field {
0 => &self.username, 0 => &self.username,
1 => &self.password, 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 { match self.current_field {
0 => &mut self.username, 0 => &mut self.username,
1 => &mut self.password, 1 => &mut self.password,
@@ -155,68 +133,57 @@ impl CanvasState for LoginState {
} }
} }
fn inputs(&self) -> Vec<&String> { pub fn current_mode(&self) -> AppMode {
vec![&self.username, &self.password] self.app_mode
} }
fn fields(&self) -> Vec<&str> { // Add missing methods that used to come from CanvasState trait
vec!["Username/Email", "Password"] pub fn has_unsaved_changes(&self) -> bool {
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes 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; self.has_unsaved_changes = changed;
} }
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
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 RegisterState {
impl CanvasState for RegisterState { pub fn new() -> Self {
fn current_field(&self) -> usize { 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 self.current_field
} }
fn current_cursor_pos(&self) -> usize { pub fn current_cursor_pos(&self) -> usize {
self.current_cursor_pos self.current_cursor_pos
} }
fn set_current_field(&mut self, index: usize) { pub fn set_current_field(&mut self, index: usize) {
if index < 5 { if index < 5 {
self.current_field = index; self.current_field = index;
// Auto-activate autocomplete when moving to role field (index 4) // Auto-activate role suggestions when moving to role field (index 4)
if index == 4 && !self.autocomplete.is_active { if index == 4 {
self.activate_autocomplete(); self.activate_role_suggestions();
} else if index != 4 && self.autocomplete.is_active { } else {
self.deactivate_autocomplete(); 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; self.current_cursor_pos = pos;
} }
fn get_current_input(&self) -> &str { pub fn get_current_input(&self) -> &str {
match self.current_field { match self.current_field {
0 => &self.username, 0 => &self.username,
1 => &self.email, 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 { match self.current_field {
0 => &mut self.username, 0 => &mut self.username,
1 => &mut self.email, 1 => &mut self.email,
@@ -238,123 +205,121 @@ impl CanvasState for RegisterState {
} }
} }
fn inputs(&self) -> Vec<&String> { pub fn current_mode(&self) -> AppMode {
vec![ self.app_mode
&self.username,
&self.email,
&self.password,
&self.password_confirmation,
&self.role,
]
} }
fn fields(&self) -> Vec<&str> { // Role suggestions management
vec![ pub fn activate_role_suggestions(&mut self) {
"Username", self.role_suggestions_active = true;
"Email (Optional)", // Filter suggestions based on current input
"Password (Optional)", let current_input = self.role.to_lowercase();
"Confirm Password", self.role_suggestions = AVAILABLE_ROLES
"Role (Optional)" .iter()
] .filter(|role| role.to_lowercase().contains(&current_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 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; self.has_unsaved_changes = changed;
} }
}
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> { // Step 2: Implement DataProvider for LoginState
match action { impl DataProvider for LoginState {
CanvasAction::Custom(action_str) if action_str == "submit" => { fn field_count(&self) -> usize {
if !self.username.is_empty() { 2
Some(format!("Submitting registration for: {}", self.username)) }
} else {
Some("Username is required".to_string()) fn field_name(&self, index: usize) -> &str {
} match index {
} 0 => "Username/Email",
_ => None, 1 => "Password",
_ => "",
} }
} }
fn current_mode(&self) -> AppMode { fn field_value(&self, index: usize) -> &str {
self.app_mode 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 // Step 3: Implement DataProvider for RegisterState
impl AutocompleteCanvasState for RegisterState { impl DataProvider for RegisterState {
type SuggestionData = String; fn field_count(&self) -> usize {
5
fn supports_autocomplete(&self, field_index: usize) -> bool {
field_index == 4 // Only role field supports autocomplete
} }
fn autocomplete_state(&self) -> Option<&AutocompleteState<Self::SuggestionData>> { fn field_name(&self, index: usize) -> &str {
Some(&self.autocomplete) match index {
} 0 => "Username",
1 => "Email (Optional)",
fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState<Self::SuggestionData>> { 2 => "Password (Optional)",
Some(&mut self.autocomplete) 3 => "Confirm Password",
} 4 => "Role (Optional)",
_ => "",
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<SuggestionItem<String>> = AVAILABLE_ROLES
.iter()
.filter(|role| role.to_lowercase().contains(&current_input))
.map(|role| SuggestionItem::simple(role.clone(), role.clone()))
.collect();
self.autocomplete.set_suggestions(filtered_suggestions);
} }
} }
fn deactivate_autocomplete(&mut self) { fn field_value(&self, index: usize) -> &str {
self.autocomplete.deactivate(); match index {
} 0 => &self.username,
1 => &self.email,
fn is_autocomplete_active(&self) -> bool { 2 => &self.password,
self.autocomplete.is_active 3 => &self.password_confirmation,
} 4 => &self.role,
_ => "",
fn is_autocomplete_ready(&self) -> bool {
self.autocomplete.is_ready()
}
fn apply_autocomplete_selection(&mut self) -> Option<String> {
// 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 set_autocomplete_suggestions(&mut self, suggestions: Vec<SuggestionItem<Self::SuggestionData>>) { fn set_field_value(&mut self, index: usize, value: String) {
if let Some(state) = self.autocomplete_state_mut() { match index {
state.set_suggestions(suggestions); 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) { fn supports_suggestions(&self, field_index: usize) -> bool {
if let Some(state) = self.autocomplete_state_mut() { field_index == 4 // only Role field supports suggestions
state.is_loading = loading;
}
} }
} }

View File

@@ -1,7 +1,8 @@
// src/state/pages/form.rs // src/state/pages/form.rs
use crate::config::colors::themes::Theme; 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 common::proto::komp_ac::search::search_response::Hit;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::Frame; use ratatui::Frame;
@@ -122,26 +123,19 @@ impl FormState {
area: Rect, area: Rect,
theme: &Theme, theme: &Theme,
is_edit_mode: bool, is_edit_mode: bool,
highlight_state: &HighlightState, // Now using canvas::HighlightState highlight_state: &HighlightState,
) { ) {
let fields_str_slice: Vec<&str> = // Wrap in FormEditor for new API
self.fields().iter().map(|s| *s).collect(); let mut editor = FormEditor::new(self.clone());
let values_str_slice: Vec<&String> = self.values.iter().collect();
// Use new canvas rendering
crate::components::form::form::render_form( canvas::render_canvas_default(f, area, &editor);
f,
area, // If autocomplete is active, render suggestions
self, if self.autocomplete_active && !self.autocomplete_suggestions.is_empty() {
&fields_str_slice, // Note: This will need to be updated when suggestions are integrated
&self.current_field, // canvas::render_suggestions_dropdown(f, area, input_rect, &canvas::DefaultCanvasTheme, &editor);
&values_str_slice, }
&self.table_name,
theme,
is_edit_mode,
highlight_state,
self.total_count,
self.current_position,
);
} }
pub fn reset_to_empty(&mut self) { pub fn reset_to_empty(&mut self) {
@@ -242,97 +236,84 @@ impl FormState {
pub fn set_readonly_mode(&mut self) { pub fn set_readonly_mode(&mut self) {
self.app_mode = AppMode::ReadOnly; self.app_mode = AppMode::ReadOnly;
} }
}
impl CanvasState for FormState { // Legacy method compatibility
fn current_field(&self) -> usize { pub fn fields(&self) -> Vec<&str> {
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> {
self.fields self.fields
.iter() .iter()
.map(|f| f.display_name.as_str()) .map(|f| f.display_name.as_str())
.collect() .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() { if index < self.fields.len() {
self.current_field = index; self.current_field = index;
} }
self.deactivate_autocomplete(); 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; self.current_cursor_pos = pos;
} }
}
fn set_has_unsaved_changes(&mut self, changed: bool) { // Step 2: Implement DataProvider for FormState
self.has_unsaved_changes = changed; impl DataProvider for FormState {
fn field_count(&self) -> usize {
self.fields.len()
} }
// --- FEATURE-SPECIFIC ACTION HANDLING --- fn field_name(&self, index: usize) -> &str {
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> { &self.fields[index].display_name
match action { }
CanvasAction::SelectSuggestion => {
if let Some(selected_idx) = self.selected_suggestion_index { fn field_value(&self, index: usize) -> &str {
if let Some(hit) = self.autocomplete_suggestions.get(selected_idx).cloned() { &self.values[index]
// Extract the value from the selected suggestion }
if let Ok(content_map) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&hit.content_json) {
let current_field_def = &self.fields[self.current_field]; fn set_field_value(&mut self, index: usize, value: String) {
if let Some(value) = content_map.get(&current_field_def.data_key) { if let Some(v) = self.values.get_mut(index) {
let new_value = json_value_to_string(value); *v = value;
let display_name = self.get_display_name_for_hit(&hit); self.has_unsaved_changes = true;
*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 get_display_value_for_field(&self, index: usize) -> &str { fn supports_suggestions(&self, field_index: usize) -> bool {
if let Some(display_text) = self.link_display_map.get(&index) { self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false)
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
} }
} }